Skip to content

Commit 838cd20

Browse files
authored
Merge pull request #3116 from redpanda-data/snowflake-auth-papercut
2 parents 8b3ab71 + ecf3e87 commit 838cd20

File tree

3 files changed

+172
-75
lines changed

3 files changed

+172
-75
lines changed

internal/impl/snowflake/auth.go

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Licensed as a Redpanda Enterprise file under the Redpanda Community
5+
* License (the "License"); you may not use this file except in compliance with
6+
* the License. You may obtain a copy of the License at
7+
*
8+
* https://github.com/redpanda-data/redpanda/blob/master/licenses/rcl.md
9+
*/
10+
11+
package snowflake
12+
13+
import (
14+
"crypto/rsa"
15+
"crypto/sha256"
16+
"crypto/x509"
17+
"encoding/base64"
18+
"encoding/pem"
19+
"errors"
20+
"fmt"
21+
"io/fs"
22+
23+
"github.com/redpanda-data/benthos/v4/public/service"
24+
"github.com/youmark/pkcs8"
25+
"golang.org/x/crypto/ssh"
26+
)
27+
28+
func wipeSlice(b []byte) {
29+
for i := range b {
30+
b[i] = '~'
31+
}
32+
}
33+
34+
// getPrivateKeyFromFile reads and parses the private key
35+
// Inspired from https://github.com/chanzuckerberg/terraform-provider-snowflake/blob/c07d5820bea7ac3d8a5037b0486c405fdf58420e/pkg/provider/provider.go#L367
36+
func getPrivateKeyFromFile(f fs.FS, path, passphrase string) (*rsa.PrivateKey, error) {
37+
privateKeyBytes, err := service.ReadFile(f, path)
38+
defer wipeSlice(privateKeyBytes)
39+
if err != nil {
40+
return nil, fmt.Errorf("failed to read private key %s: %s", path, err)
41+
}
42+
if len(privateKeyBytes) == 0 {
43+
return nil, errors.New("private key is empty")
44+
}
45+
return getPrivateKey(privateKeyBytes, passphrase)
46+
}
47+
48+
func getPrivateKey(privateKeyBytes []byte, passphrase string) (*rsa.PrivateKey, error) {
49+
privateKeyBlock, _ := pem.Decode(privateKeyBytes)
50+
if privateKeyBlock == nil {
51+
// Snowflake generally uses base64 encoded keys everywhere not pem encoding,
52+
// so let's be compatible with that as a fallback.
53+
dbuf := make([]byte, base64.StdEncoding.DecodedLen(len(privateKeyBytes)))
54+
n, err := base64.StdEncoding.Decode(dbuf, privateKeyBytes)
55+
if err != nil {
56+
return nil, errors.New("could not parse private key, key is not in PEM format")
57+
}
58+
privateKeyBlock = &pem.Block{
59+
Type: "PRIVATE KEY",
60+
Bytes: dbuf[:n],
61+
}
62+
if passphrase != "" {
63+
privateKeyBlock.Type = "ENCRYPTED PRIVATE KEY"
64+
}
65+
privateKeyBytes = pem.EncodeToMemory(privateKeyBlock)
66+
}
67+
68+
if privateKeyBlock.Type == "ENCRYPTED PRIVATE KEY" {
69+
if passphrase == "" {
70+
return nil, errors.New("private key requires a passphrase, but private_key_pass was not supplied")
71+
}
72+
73+
// Only keys encrypted with pbes2 http://oid-info.com/get/1.2.840.113549.1.5.13 are supported.
74+
// pbeWithMD5AndDES-CBC http://oid-info.com/get/1.2.840.113549.1.5.3 is not supported.
75+
privateKey, err := pkcs8.ParsePKCS8PrivateKeyRSA(privateKeyBlock.Bytes, []byte(passphrase))
76+
if err != nil {
77+
return nil, fmt.Errorf("failed to decrypt encrypted private key (only ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc are supported): %s", err)
78+
}
79+
80+
return privateKey, nil
81+
}
82+
83+
privateKey, err := ssh.ParseRawPrivateKey(privateKeyBytes)
84+
if err != nil {
85+
return nil, fmt.Errorf("could not parse private key: %s", err)
86+
}
87+
88+
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
89+
if !ok {
90+
return nil, fmt.Errorf("private key must be of type RSA but got %T instead: ", privateKey)
91+
}
92+
return rsaPrivateKey, nil
93+
}
94+
95+
// calculatePublicKeyFingerprint computes the value of the `RSA_PUBLIC_KEY_FP` for the current user based on the
96+
// configured private key
97+
// Inspired from https://stackoverflow.com/questions/63598044/snowpipe-rest-api-returning-always-invalid-jwt-token
98+
func calculatePublicKeyFingerprint(privateKey *rsa.PrivateKey) (string, error) {
99+
pubKey := privateKey.Public()
100+
pubDER, err := x509.MarshalPKIXPublicKey(pubKey)
101+
if err != nil {
102+
return "", fmt.Errorf("failed to marshal public key: %s", err)
103+
}
104+
105+
hash := sha256.Sum256(pubDER)
106+
return "SHA256:" + base64.StdEncoding.EncodeToString(hash[:]), nil
107+
}

internal/impl/snowflake/auth_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2025 Redpanda Data, Inc.
3+
*
4+
* Licensed as a Redpanda Enterprise file under the Redpanda Community
5+
* License (the "License"); you may not use this file except in compliance with
6+
* the License. You may obtain a copy of the License at
7+
*
8+
* https://github.com/redpanda-data/redpanda/blob/master/licenses/rcl.md
9+
*/
10+
11+
package snowflake
12+
13+
import (
14+
"crypto/rand"
15+
"crypto/rsa"
16+
"crypto/x509"
17+
"encoding/base64"
18+
"encoding/pem"
19+
"testing"
20+
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
func generatePrivateKey() ([]byte, error) {
25+
const keySize = 2048
26+
privateKey, err := rsa.GenerateKey(rand.Reader, keySize)
27+
if err != nil {
28+
return nil, err
29+
}
30+
return x509.MarshalPKCS8PrivateKey(privateKey)
31+
}
32+
33+
func generateBase64EncodedKey() ([]byte, error) {
34+
privDER, err := generatePrivateKey()
35+
if err != nil {
36+
return nil, err
37+
}
38+
return []byte(base64.StdEncoding.EncodeToString(privDER)), nil
39+
}
40+
41+
func generatePEMEncodedKey() ([]byte, error) {
42+
privDER, err := generatePrivateKey()
43+
if err != nil {
44+
return nil, err
45+
}
46+
privBlock := &pem.Block{
47+
Type: "PRIVATE KEY",
48+
Bytes: privDER,
49+
}
50+
return pem.EncodeToMemory(privBlock), nil
51+
}
52+
53+
func TestPrivateKeyPemEncoded(t *testing.T) {
54+
k, err := generatePEMEncodedKey()
55+
require.NoError(t, err)
56+
_, err = getPrivateKey(k, "")
57+
require.NoError(t, err)
58+
}
59+
60+
func TestPrivateKeyBase64Encoded(t *testing.T) {
61+
k, err := generateBase64EncodedKey()
62+
require.NoError(t, err)
63+
_, err = getPrivateKey(k, "")
64+
require.NoError(t, err)
65+
}

internal/impl/snowflake/output_snowflake_put.go

-75
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,9 @@ import (
1212
"bytes"
1313
"context"
1414
"crypto/rsa"
15-
"crypto/sha256"
16-
"crypto/x509"
1715
"database/sql"
18-
"encoding/base64"
1916
"encoding/json"
20-
"encoding/pem"
21-
"errors"
2217
"fmt"
23-
"io/fs"
2418
"net/http"
2519
"net/url"
2620
"path"
@@ -31,8 +25,6 @@ import (
3125
"github.com/gofrs/uuid"
3226
"github.com/golang-jwt/jwt/v5"
3327
"github.com/snowflakedb/gosnowflake"
34-
"github.com/youmark/pkcs8"
35-
"golang.org/x/crypto/ssh"
3628

3729
"github.com/redpanda-data/benthos/v4/public/service"
3830

@@ -423,73 +415,6 @@ func init() {
423415

424416
//------------------------------------------------------------------------------
425417

426-
func wipeSlice(b []byte) {
427-
for i := range b {
428-
b[i] = '~'
429-
}
430-
}
431-
432-
// getPrivateKeyFromFile reads and parses the private key
433-
// Inspired from https://github.com/chanzuckerberg/terraform-provider-snowflake/blob/c07d5820bea7ac3d8a5037b0486c405fdf58420e/pkg/provider/provider.go#L367
434-
func getPrivateKeyFromFile(f fs.FS, path, passphrase string) (*rsa.PrivateKey, error) {
435-
privateKeyBytes, err := service.ReadFile(f, path)
436-
defer wipeSlice(privateKeyBytes)
437-
if err != nil {
438-
return nil, fmt.Errorf("failed to read private key %s: %s", path, err)
439-
}
440-
if len(privateKeyBytes) == 0 {
441-
return nil, errors.New("private key is empty")
442-
}
443-
return getPrivateKey(privateKeyBytes, passphrase)
444-
}
445-
446-
func getPrivateKey(privateKeyBytes []byte, passphrase string) (*rsa.PrivateKey, error) {
447-
privateKeyBlock, _ := pem.Decode(privateKeyBytes)
448-
if privateKeyBlock == nil {
449-
return nil, errors.New("could not parse private key, key is not in PEM format")
450-
}
451-
452-
if privateKeyBlock.Type == "ENCRYPTED PRIVATE KEY" {
453-
if passphrase == "" {
454-
return nil, errors.New("private key requires a passphrase, but private_key_pass was not supplied")
455-
}
456-
457-
// Only keys encrypted with pbes2 http://oid-info.com/get/1.2.840.113549.1.5.13 are supported.
458-
// pbeWithMD5AndDES-CBC http://oid-info.com/get/1.2.840.113549.1.5.3 is not supported.
459-
privateKey, err := pkcs8.ParsePKCS8PrivateKeyRSA(privateKeyBlock.Bytes, []byte(passphrase))
460-
if err != nil {
461-
return nil, fmt.Errorf("failed to decrypt encrypted private key (only ciphers aes-128-cbc, aes-128-gcm, aes-192-cbc, aes-192-gcm, aes-256-cbc, aes-256-gcm, and des-ede3-cbc are supported): %s", err)
462-
}
463-
464-
return privateKey, nil
465-
}
466-
467-
privateKey, err := ssh.ParseRawPrivateKey(privateKeyBytes)
468-
if err != nil {
469-
return nil, fmt.Errorf("could not parse private key: %s", err)
470-
}
471-
472-
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
473-
if !ok {
474-
return nil, fmt.Errorf("private key must be of type RSA but got %T instead: ", privateKey)
475-
}
476-
return rsaPrivateKey, nil
477-
}
478-
479-
// calculatePublicKeyFingerprint computes the value of the `RSA_PUBLIC_KEY_FP` for the current user based on the
480-
// configured private key
481-
// Inspired from https://stackoverflow.com/questions/63598044/snowpipe-rest-api-returning-always-invalid-jwt-token
482-
func calculatePublicKeyFingerprint(privateKey *rsa.PrivateKey) (string, error) {
483-
pubKey := privateKey.Public()
484-
pubDER, err := x509.MarshalPKIXPublicKey(pubKey)
485-
if err != nil {
486-
return "", fmt.Errorf("failed to marshal public key: %s", err)
487-
}
488-
489-
hash := sha256.Sum256(pubDER)
490-
return "SHA256:" + base64.StdEncoding.EncodeToString(hash[:]), nil
491-
}
492-
493418
type dbI interface {
494419
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
495420
Close() error

0 commit comments

Comments
 (0)