diff --git a/.changelog/71a17e4b0d334a06a7b2398a0dc1ec87.json b/.changelog/71a17e4b0d334a06a7b2398a0dc1ec87.json new file mode 100644 index 00000000000..0743bf5f154 --- /dev/null +++ b/.changelog/71a17e4b0d334a06a7b2398a0dc1ec87.json @@ -0,0 +1,8 @@ +{ + "id": "71a17e4b-0d33-4a06-a7b2-398a0dc1ec87", + "type": "feature", + "description": "Add support for loading PKCS8-formatted private keys.", + "modules": [ + "feature/cloudfront/sign" + ] +} \ No newline at end of file diff --git a/feature/cloudfront/sign/privkey.go b/feature/cloudfront/sign/privkey.go index cb3113684c0..d95bb0ab631 100644 --- a/feature/cloudfront/sign/privkey.go +++ b/feature/cloudfront/sign/privkey.go @@ -1,6 +1,9 @@ package sign import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" "crypto/x509" "encoding/pem" @@ -44,6 +47,10 @@ func LoadPEMPrivKey(reader io.Reader) (*rsa.PrivateKey, error) { // LoadEncryptedPEMPrivKey decrypts the PEM encoded private key using the // password provided returning a RSA private key. If the PEM data is invalid, // or unable to decrypt an error will be returned. +// +// Deprecated: RFC 1423 PEM encryption is insecure. Callers using encrypted +// keys should instead decrypt that payload externally and pass it to +// [LoadPEMPrivKey]. func LoadEncryptedPEMPrivKey(reader io.Reader, password []byte) (*rsa.PrivateKey, error) { block, err := loadPem(reader) if err != nil { @@ -58,6 +65,66 @@ func LoadEncryptedPEMPrivKey(reader io.Reader, password []byte) (*rsa.PrivateKey return x509.ParsePKCS1PrivateKey(decryptedBlock) } +// LoadPEMPrivKeyPKCS8 reads a PEM-encoded RSA private key in PKCS8 format from +// the given reader. +// +// x509.ParsePKCS8PrivateKey can return multiple key types and this API does +// not discern between them. Callers in need of the underlying value must +// obtain it via type assertion: +// +// key, err := LoadPEMPrivKeyPKCS8(r) +// if err != nil { /* ... */ } +// +// switch key.(type) { +// case *rsa.PrivateKey: +// // ... +// case *ecdsa.PrivateKey: +// // ... +// case ed25519.PrivateKey: +// // ... +// default: +// panic("unrecognized private key type") +// } +// +// See aforementioned API docs for a full list of possible key types. +// +// If calling code can opaquely handle the returned key as a crypto.Signer, use +// [LoadPEMPrivKeyPKCS8AsSigner] instead. +func LoadPEMPrivKeyPKCS8(reader io.Reader) (interface{}, error) { + block, err := loadPem(reader) + if err != nil { + return nil, fmt.Errorf("load pem: %v", err) + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("parse pkcs8 key: %v", err) + } + + return key, nil +} + +var ( + _ crypto.Signer = (*rsa.PrivateKey)(nil) + _ crypto.Signer = (*ecdsa.PrivateKey)(nil) + _ crypto.Signer = (ed25519.PrivateKey)(nil) +) + +// LoadPEMPrivKeyPKCS8AsSigner wraps [LoadPEMPrivKeyPKCS8] to expect a crypto.Signer. +func LoadPEMPrivKeyPKCS8AsSigner(reader io.Reader) (crypto.Signer, error) { + key, err := LoadPEMPrivKeyPKCS8(reader) + if err != nil { + return nil, fmt.Errorf("load key: %v", err) + } + + signer, ok := key.(crypto.Signer) + if !ok { + return nil, fmt.Errorf("key of type %T is not a crypto.Signer", key) + } + + return signer, nil +} + func loadPem(reader io.Reader) (*pem.Block, error) { b, err := ioutil.ReadAll(reader) if err != nil { diff --git a/feature/cloudfront/sign/privkey_test.go b/feature/cloudfront/sign/privkey_test.go index 84750d8f589..be2b5994498 100644 --- a/feature/cloudfront/sign/privkey_test.go +++ b/feature/cloudfront/sign/privkey_test.go @@ -2,9 +2,14 @@ package sign import ( "bytes" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + cryptorand "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" + "fmt" "io" "math/rand" "strings" @@ -88,3 +93,123 @@ func TestLoadEncryptedPEMPrivKeyWrongPassword(t *testing.T) { t.Errorf("Expected nil privKey but got %#v", privKey) } } + +func TestLoadPEMPrivKeyPKCS8_RSA(t *testing.T) { + r, err := generatePKCS8(keyTypeRSA) + if err != nil { + t.Errorf("generate pkcs8 key: %v", err) + } + + var rr bytes.Buffer + tee := io.TeeReader(r, &rr) + + key, err := LoadPEMPrivKeyPKCS8(tee) + if err != nil { + t.Errorf("load pkcs8 key: %v", err) + } + + if _, ok := key.(*rsa.PrivateKey); !ok { + t.Errorf("key should be rsa but was %T", key) + } + + _, err = LoadPEMPrivKeyPKCS8AsSigner(&rr) + if err != nil { + t.Errorf("load pkcs8 key as signer: %v", err) + } +} + +func TestLoadPEMPrivKeyPKCS8_ECDSA(t *testing.T) { + r, err := generatePKCS8(keyTypeECDSA) + if err != nil { + t.Errorf("generate pkcs8 key: %v", err) + } + + var rr bytes.Buffer + tee := io.TeeReader(r, &rr) + + key, err := LoadPEMPrivKeyPKCS8(tee) + if err != nil { + t.Errorf("load pkcs8 key: %v", err) + } + + if _, ok := key.(*ecdsa.PrivateKey); !ok { + t.Errorf("key should be ecdsa but was %T", key) + } + + _, err = LoadPEMPrivKeyPKCS8AsSigner(&rr) + if err != nil { + t.Errorf("load pkcs8 key as signer: %v", err) + } +} + +func TestLoadPEMPrivKeyPKCS8_ED25519(t *testing.T) { + r, err := generatePKCS8(keyTypeED25519) + if err != nil { + t.Errorf("generate pkcs8 key: %v", err) + } + + var rr bytes.Buffer + tee := io.TeeReader(r, &rr) + + key, err := LoadPEMPrivKeyPKCS8(tee) + if err != nil { + t.Errorf("load pkcs8 key: %v", err) + } + + if _, ok := key.(ed25519.PrivateKey); !ok { + t.Errorf("key should be ed25519 but was %T", key) + } + + _, err = LoadPEMPrivKeyPKCS8AsSigner(&rr) + if err != nil { + t.Errorf("load pkcs8 key as signer: %v", err) + } +} + +type keyType int + +const ( + keyTypeRSA keyType = iota + keyTypeECDSA + keyTypeED25519 +) + +func generatePKCS8(typ keyType) (io.Reader, error) { + key, pemType, err := generatePrivateKey(typ) + if err != nil { + return nil, err + } + + b, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, err + } + + block := &pem.Block{ + Type: pemType, + Bytes: b, + } + + var buf bytes.Buffer + if err := pem.Encode(&buf, block); err != nil { + return nil, err + } + + return &buf, err +} + +func generatePrivateKey(typ keyType) (interface{}, string, error) { + switch typ { + case keyTypeRSA: + key, err := rsa.GenerateKey(cryptorand.Reader, 1024) + return key, "RSA PRIVATE KEY", err + case keyTypeECDSA: + key, err := ecdsa.GenerateKey(elliptic.P224(), cryptorand.Reader) + return key, "ECDSA PRIVATE KEY", err + case keyTypeED25519: + _, key, err := ed25519.GenerateKey(cryptorand.Reader) + return key, "ED25519 PRIVATE KEY", err + default: + return nil, "", fmt.Errorf("unsupported key type %v", typ) + } +} diff --git a/feature/cloudfront/sign/sign_url.go b/feature/cloudfront/sign/sign_url.go index cca65e884e1..02c8f3e0536 100644 --- a/feature/cloudfront/sign/sign_url.go +++ b/feature/cloudfront/sign/sign_url.go @@ -3,14 +3,23 @@ // More information about signed URLs and their structure can be found at: // http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-creating-signed-url-canned-policy.html // -// To sign a URL create a URLSigner with your private key and credential pair key ID. -// Once you have a URLSigner instance you can call Sign or SignWithPolicy to -// sign the URLs. +// To sign a URL create a [URLSigner] with your private key and credential pair +// key ID. Once you have a URLSigner instance you can call [URLSigner.Sign] or +// [URLSigner.SignWithPolicy] to sign the URLs. // // Example: // -// // Sign URL to be valid for 1 hour from now. +// // Load our key from a PEM block. +// privKey, err := sign.LoadPEMPrivKey(block) +// if err != nil { +// log.Fatalf("Failed to load private key, err: %s\n", err.Error()) +// } +// +// // Create our signer. Keys loaded via the LoadPEMPrivKey* family of APIs +// // implement crypto.Signer and can be passed to this directly. // signer := sign.NewURLSigner(keyID, privKey) +// +// // Sign URL to be valid for 1 hour from now. // signedURL, err := signer.Sign(rawURL, time.Now().Add(1*time.Hour)) // if err != nil { // log.Fatalf("Failed to sign url, err: %s\n", err.Error())