diff --git a/ast/builtins.go b/ast/builtins.go index fa882da627..63ba033658 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -211,6 +211,7 @@ var DefaultBuiltins = [...]*Builtin{ CryptoX509ParseCertificateRequest, CryptoX509ParseRSAPrivateKey, CryptoX509ParseKeyPair, + CryptoParsePrivateKeys, CryptoHmacMd5, CryptoHmacSha1, CryptoHmacSha256, @@ -2312,6 +2313,19 @@ var CryptoX509ParseRSAPrivateKey = &Builtin{ ), } +var CryptoParsePrivateKeys = &Builtin{ + Name: "crypto.parse_private_keys", + Description: `Returns zero or more private keys from the given encoded string containing DER certificate data. + +If the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.`, + Decl: types.NewFunction( + types.Args( + types.Named("keys", types.S).Description("PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded."), + ), + types.Named("output", types.NewArray(nil, types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)))).Description("parsed private keys represented as objects"), + ), +} + var CryptoMd5 = &Builtin{ Name: "crypto.md5", Description: "Returns a string representing the input string hashed with the MD5 function", diff --git a/builtin_metadata.json b/builtin_metadata.json index 0cf978f201..22a0d5654c 100644 --- a/builtin_metadata.json +++ b/builtin_metadata.json @@ -39,6 +39,7 @@ "crypto.hmac.sha256", "crypto.hmac.sha512", "crypto.md5", + "crypto.parse_private_keys", "crypto.sha1", "crypto.sha256", "crypto.x509.parse_and_verify_certificates", @@ -3712,6 +3713,26 @@ }, "wasm": false }, + "crypto.parse_private_keys": { + "args": [ + { + "description": "PEM encoded data containing one or more private keys as concatenated blocks. Optionally Base64 encoded.", + "name": "keys", + "type": "string" + } + ], + "available": [ + "edge" + ], + "description": "Returns zero or more private keys from the given encoded string containing DER certificate data.\n\nIf the input is empty, the function will return null. The input string should be a list of one or more concatenated PEM blocks. The whole input of concatenated PEM blocks can optionally be Base64 encoded.", + "introduced": "edge", + "result": { + "description": "parsed private keys represented as objects", + "name": "output", + "type": "array[object[string: any]]" + }, + "wasm": false + }, "crypto.sha1": { "args": [ { diff --git a/capabilities.json b/capabilities.json index a89d1f859e..1b0b80322b 100644 --- a/capabilities.json +++ b/capabilities.json @@ -671,6 +671,31 @@ "type": "function" } }, + { + "name": "crypto.parse_private_keys", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "type": "function" + } + }, { "name": "crypto.sha1", "decl": { diff --git a/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml b/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml new file mode 100644 index 0000000000..1ceb5f580f --- /dev/null +++ b/test/cases/testdata/cryptoparsersaprivatekeys/test-cryptoparsersaprivatekey-1.yaml @@ -0,0 +1,35 @@ +cases: + - data: + modules: + - | + package testing + + pem := "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA9D/bK4171aiTNUkrUCHKGMLSQooV+o3wdz2889h9iv0HhhBJ\nCAGU54K3duB8ofHpmYL50QodcR4RLw1vSkaI+FPdPDMyKxKj/YcmofJjz4kW+Iqw\nFbBcbMnKnEVzye+CyW9YYOTu0xWtcgen80zGp2opG0GZX86hBjjXJnjOdrJTk6x2\nNAiJIbjsQevysmj+2MyqVm8widxw0x+rGhTaCD+ZXWitN0a0WO1aaA8c/7i99I9z\nhe2peKvXzEtMaqYO9ptHcYmq2z0QWvZuJVMv5Yn0mScLWyh91R099IOtn6sNaMMs\nOyTpi7E/2IlVgI2uKGPEopKkMFV8Fl2YaAbo7wIDAQABAoIBAAyMZ08ygqU0dvOq\n4a3JPp/NCo5el8h6mFsX8eg5PCHy4/sQRSBDLIpEXfaei+iqDA1V/E2wDlksaUeY\nkhony4uui1Q3cSFjYMd6tRJm6JfV/DcisO88U1NHfsBOlSdPxdFhhhHcUSTJHVMZ\nb5iBXkdlnd0HnsCcVguCyhLw6/KPFyiA+NYRz68flxze7admyVp5C6i/HbMPq8Pr\nMilBUvOFtxuaGeJBAiavuzUe9I70dRwpe424tMvisSA8h7Xbm8BeN/PJHDV/2JrI\nURgQ563yQ5So/Qg8AgxXRkpgWM9zAh7r31PBO86vq/B4ZbON/TtWdcZVsAcVB4Pk\ntqc8JNkCgYEA+g8V+y92SETdcwUkbd5O9Fg5CkfdsALsBXVH6FunrCUV5HS9l5o6\nMMBbJ/08odW/bP5BmOa4A/Hbk9uG/UfQn2KQ3HCgPlxUEwQO07R1/FcQOe4xmyG6\nJpDgQ30viE1RtlCkceQWUeitCIqZsYu0i8sLZLWJH+V/07OB4G17ELMCgYEA+g1v\nhrlAFNhZvrIX/zcP3xF2pZ+AqkFXdL/tWQZkWAVToONn/LlXTH71C/TO2x+OaQRm\nqX1bA9Zhyjf1gYQN9RenjUswvggk0aY2Tk28wUqowMGSsjQHmZ20EphHNMWNJpdS\nfKFfrQIFKCnLlpQVNz+j3bLWZUnq+jPaYnJP7NUCgYEA48qcVo7c7Ga3aNEVZ3St\nbg90HrZq760pvqshDz13V+0MrWnfUFxxh/mi0KHy+uYRlMNllFkQ5p8LTP0dUlt6\nY8dReU6r20MWX6BBtX9eP7o8ENm4nL4zqnAtq609gKgWuMNrmkiSQJl6Dx7bdY5z\nsSkNPvfUa5cQRBTxSjXRdtsCgYBHrzpdwRXh4/Q2ew/uFnbyWCtPZ96W8IyF58+/\nSdnSchR7dzYEeY3RXEQb3V6/6tgEu0JDLLC+9OKr+kbjjlwB+3oJQ5kBoYwMnj3L\nTPXj4+dk+xl3BPt4yoEpI4amVkwU2CTJnemzy3R3AyReUq2SXSg5El/sQbifaeYd\neu/20QKBgH/5IZHGBKiRAe1ww2FzOpDtL8VXXTe3EAXKutfajrHTqPz9+lXknX/D\nUMosh264nYXYS29WqxhJVutbE9u8e0VpuY1qIN9/3R0WKfTLTMUFlZtbqTepvsy1\nW2UbK732I4Nfp0/mtUvOSdMZO8dxbSdEeMnw/Ec8QgxK9a1rRu9+\n-----END RSA PRIVATE KEY-----" + + p { + count(crypto.parse_private_keys(pem)) == 1 + } + note: cryptoparseprivatekey/valid + query: data.testing.p = x + want_result: + - x: true + - data: + modules: + - | + package testing + pem := "nope" + p := crypto.parse_private_keys(pem) + note: cryptoparseprivatekey/invalid + query: data.testing.p = x + want_result: + - x: [] + - data: + modules: + - | + package testing + pem := "" + p := crypto.parse_private_keys(pem) + note: cryptoparseprivatekey/invalid + query: data.testing.p = x + want_result: + - x: null \ No newline at end of file diff --git a/topdown/crypto.go b/topdown/crypto.go index 5c4cf1b973..c33abb2c93 100644 --- a/topdown/crypto.go +++ b/topdown/crypto.go @@ -6,6 +6,7 @@ package topdown import ( "bytes" + "crypto" "crypto/hmac" "crypto/md5" "crypto/sha1" @@ -21,8 +22,9 @@ import ( "os" "strings" - "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/internal/jwx/jwk" + + "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/topdown/builtins" "github.com/open-policy-agent/opa/util" ) @@ -39,7 +41,8 @@ const ( blockTypeRSAPrivateKey = "RSA PRIVATE KEY" // blockTypeRSAPrivateKey indicates this PEM block contains a RSA private key. // Exported for tests. - blockTypePrivateKey = "PRIVATE KEY" + blockTypePrivateKey = "PRIVATE KEY" + blockTypeEcPrivateKey = "EC PRIVATE KEY" ) func builtinCryptoX509ParseCertificates(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { @@ -168,7 +171,8 @@ func builtinCryptoX509ParseCertificateRequest(_ BuiltinContext, operands []*ast. return iter(ast.NewTerm(v)) } -func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { +func builtinCryptoJWKFromPrivateKey(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + var x interface{} a := operands[0].Value input, err := builtins.StringOperand(a, 1) @@ -177,23 +181,83 @@ func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, operands []*ast.Term, } // get the raw private key - rawKey, err := getRSAPrivateKeyFromString(string(input)) + pemDataString := string(input) + + if pemDataString == "" { + return fmt.Errorf("input PEM data was empty") + } + + // This built in must be supplied a valid PEM or base64 encoded string. + // If the input is not a PEM string, attempt to decode b64. + // If the base64 decode fails - this is an error + if !strings.HasPrefix(pemDataString, "-----BEGIN") { + bs, err := base64.StdEncoding.DecodeString(pemDataString) + if err != nil { + return err + } + pemDataString = string(bs) + } + + rawKeys, err := getPrivateKeysFromPEMData(pemDataString) if err != nil { return err } - rsaPrivateKey, err := jwk.New(rawKey) + if len(rawKeys) == 0 { + return iter(ast.NullTerm()) + } + + key, err := jwk.New(rawKeys[0]) if err != nil { return err } - jsonKey, err := json.Marshal(rsaPrivateKey) + jsonKey, err := json.Marshal(key) + if err != nil { + return err + } + + if err := util.UnmarshalJSON(jsonKey, &x); err != nil { + return err + } + + value, err := ast.InterfaceToValue(x) + if err != nil { + return err + } + + return iter(ast.NewTerm(value)) +} + +func builtinCryptoParsePrivateKeys(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error { + + a := operands[0].Value + input, err := builtins.StringOperand(a, 1) + if err != nil { + return err + } + + if string(input) == "" { + return iter(ast.NullTerm()) + } + + // get the raw private key + rawKeys, err := getPrivateKeysFromPEMData(string(input)) + if err != nil { + return err + } + + if len(rawKeys) == 0 { + return iter(ast.NewTerm(ast.NewArray())) + } + + bs, err := json.Marshal(rawKeys) if err != nil { return err } var x interface{} - if err := util.UnmarshalJSON(jsonKey, &x); err != nil { + if err := util.UnmarshalJSON(bs, &x); err != nil { return err } @@ -298,7 +362,8 @@ func init() { RegisterBuiltinFunc(ast.CryptoSha1.Name, builtinCryptoSha1) RegisterBuiltinFunc(ast.CryptoSha256.Name, builtinCryptoSha256) RegisterBuiltinFunc(ast.CryptoX509ParseCertificateRequest.Name, builtinCryptoX509ParseCertificateRequest) - RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoX509ParseRSAPrivateKey) + RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoJWKFromPrivateKey) + RegisterBuiltinFunc(ast.CryptoParsePrivateKeys.Name, builtinCryptoParsePrivateKeys) RegisterBuiltinFunc(ast.CryptoX509ParseKeyPair.Name, builtinCryptoX509ParseKeyPair) RegisterBuiltinFunc(ast.CryptoHmacMd5.Name, builtinCryptoHmacMd5) RegisterBuiltinFunc(ast.CryptoHmacSha1.Name, builtinCryptoHmacSha1) @@ -378,43 +443,56 @@ func getX509CertsFromPem(pemBlocks []byte) ([]*x509.Certificate, error) { return x509.ParseCertificates(decodedCerts) } -func getRSAPrivateKeyFromString(key string) (interface{}, error) { - // if the input is PEM handle that - if strings.HasPrefix(key, "-----BEGIN") { - return getRSAPrivateKeyFromPEM([]byte(key)) +func getPrivateKeysFromPEMData(pemData string) ([]crypto.PrivateKey, error) { + pemBlockString := pemData + + var validPrivateKeys []crypto.PrivateKey + + // if the input is base64, decode it + bs, err := base64.StdEncoding.DecodeString(pemBlockString) + if err == nil { + pemBlockString = string(bs) } + bs = []byte(pemBlockString) - // assume input is base64 if not PEM - b64, err := base64.StdEncoding.DecodeString(key) - if err != nil { - return nil, err + for len(bs) > 0 { + inputLen := len(bs) + var block *pem.Block + block, bs = pem.Decode(bs) + if block == nil && len(bs) == 0 { + break + } + // should only happen if end of input is not a valid PEM block. See TestParseRSAPrivateKeyVariedPemInput. + if inputLen == len(bs) { + break + } + + if block == nil { + continue + } + + switch block.Type { + case blockTypeRSAPrivateKey: + parsedKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + case blockTypePrivateKey: + parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + case blockTypeEcPrivateKey: + parsedKey, err := x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, err + } + validPrivateKeys = append(validPrivateKeys, parsedKey) + } } - - return getRSAPrivateKeyFromPEM(b64) -} - -func getRSAPrivateKeyFromPEM(pemBlocks []byte) (interface{}, error) { - - // decode the pem into the Block struct - p, _ := pem.Decode(pemBlocks) - if p == nil { - return nil, fmt.Errorf("failed to parse PEM block containing the key") - } - - // if the key is in PKCS1 format - if p.Type == blockTypeRSAPrivateKey { - return x509.ParsePKCS1PrivateKey(p.Bytes) - } - - // if the key is in PKCS8 format - if p.Type == blockTypePrivateKey { - return x509.ParsePKCS8PrivateKey(p.Bytes) - } - - // unsupported key format - return nil, fmt.Errorf("PEM block type is '%s', expected %s or %s", p.Type, blockTypeRSAPrivateKey, - blockTypePrivateKey) - + return validPrivateKeys, nil } // addCACertsFromFile adds CA certificates from filePath into the given pool. diff --git a/topdown/crypto_test.go b/topdown/crypto_test.go index bdfa49048e..e70476df7e 100644 --- a/topdown/crypto_test.go +++ b/topdown/crypto_test.go @@ -1,15 +1,17 @@ package topdown import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "encoding/base64" + "fmt" "strings" "testing" - - "github.com/open-policy-agent/opa/internal/jwx/jwk" ) -func TestX509ParseAndVerify(t *testing.T) { - rootCA := `-----BEGIN CERTIFICATE----- +var rootCA = `-----BEGIN CERTIFICATE----- MIIBoDCCAUagAwIBAgIRAJXcMYZALXooNq/VV/grXhMwCgYIKoZIzj0EAwIwLjER MA8GA1UEChMIT1BBIFRlc3QxGTAXBgNVBAMTEE9QQSBUZXN0IFJvb3QgQ0EwHhcN MjEwNzAxMTc0MTUzWhcNMzEwNjI5MTc0MTUzWjAuMREwDwYDVQQKEwhPUEEgVGVz @@ -20,7 +22,7 @@ HRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBTch60qxQvLl+AfDfcaXmjvT8GvpzAK BggqhkjOPQQDAgNIADBFAiBqraIP0l2U0oNuH0+rf36hDks94wSB5EGlGH3lYNMR ugIhANkbukX5hOP8pJDRWP/pYuv6MBnRY4BS8gpp9Vu31qOb -----END CERTIFICATE-----` - intermediateCA := `-----BEGIN CERTIFICATE----- +var intermediateCA = `-----BEGIN CERTIFICATE----- MIIByDCCAW6gAwIBAgIQC0k4DPGrh9me73EJX5zntTAKBggqhkjOPQQDAjAuMREw DwYDVQQKEwhPUEEgVGVzdDEZMBcGA1UEAxMQT1BBIFRlc3QgUm9vdCBDQTAeFw0y MTA3MDExNzQxNTNaFw0zMTA2MjkxNzQxNTNaMDYxETAPBgNVBAoTCE9QQSBUZXN0 @@ -32,7 +34,7 @@ ALy/9WEwHwYDVR0jBBgwFoAU3IetKsULy5fgHw33Gl5o70/Br6cwCgYIKoZIzj0E AwIDSAAwRQIgUwsYApW9Tsm6AstWswaKGie0srB4FUkUbfKwWmUI2JgCIQCBTySN MF+EiQAMKyz/N9KUuXEckC356WvKcyJaYYcV0w== -----END CERTIFICATE-----` - leaf := `-----BEGIN CERTIFICATE----- +var leaf = `-----BEGIN CERTIFICATE----- MIIB8zCCAZqgAwIBAgIRAID4gPKg7DDiuOfzUYFSXLAwCgYIKoZIzj0EAwIwNjER MA8GA1UEChMIT1BBIFRlc3QxITAfBgNVBAMTGE9QQSBUZXN0IEludGVybWVkaWF0 ZSBDQTAeFw0yMTA3MDUxNzQ5NTBaFw0zNjA3MDExNzQ5NDdaMCUxIzAhBgNVBAMT @@ -46,6 +48,100 @@ ADBEAiAtmZewL94ijN0YwUGaJM9BXCaoTQPwkzugqjCj+K912QIgKKFvbPu4asrE nwy7dzejHmQUcZ/aUNbc4VTbiv15ESk= -----END CERTIFICATE-----` +var rsaPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEA3Y8cXdK06ufUSP035jiwJk8IsuwGjJD/LSRvE2AhJL/Vp9mu +41z1bV5Mi/TTK/uZNqv6VdvTxFPZOUYycLXEchg8L6wrOLgAX0DleP+YTKGG4oyg +dTZZcqzwr4p7WhYzLFmpW8RCLgHJbV0fF1pejJKtV+9fpsdX8oQzKvqO39ne1hl+ +m/lq2LKBK0z03c4ay+bFzA8AFMndmzfB3uXl2fTFsNaoYxAkGwlcvFAXNegPKtaf +9Co5JpRlRejPYVSonCvCvBakGIDCRb0ZHQrcGBzDnqjZeZMDkfe0YKoRUR+JFn69 +C7a4tHheA0TerIDcv+IqadY7p2jwIom9di1oWwIDAQABAoIBAQDXEXGGvd+y20Gd +bHhTuZl8RmH6VNTypFmf92r/UuQ5aSI8Ijn7KKRw+wWxIgHPAxcyE/UYXSCOxpnp +V/Pkpv0/h7j8ydLW5v4teLCIKQws7ushhULJJO3lPG0S6Yld5IjeN1cH5lYblM5z +o95na+i16jfsUUf3fDAqERweT0Rbk7IlegTgXtXLjbvGpFWgjH7Oc8UPpy56i05h +NtdBvQhFV8LMckQAfEinBTPDHqZw6hGIfJtieRhwTzGh5H0fnDCRZanRKm2uxh4Z +9ciYZ/wa0Af23atGoax1YbQJFJK8h0vWcL1jJkaZ+CmVmRtYcWPTpDNGe2FQn9I2 +EwF5nB8BAoGBAPpAsZiFC00YJf1gN4G588+7hxMU2BaoTosImSD27sLLmE2XHBa+ +FrtLJR+t6pRtt7aQccGrNp2G234ucjitM2A1JmtzywPhtAXp+/VaguikdJ62zAjl +Sn6nl9W6ovOQ0NsHGmO7MFILrWXXpF7IqhXd/MdwMnxJABsKqZpBLB2BAoGBAOKl +uARPETauBRdQisEzHI1kosHigCVCSTwwTnFa8LXfinfFCq68SuuwqUdN5RaNUpGx +zTFxOgihcSlfOF0/VXROi6PI768pp2SOgbKjXsleZqxaSe5iZ61jt0uU0HlUsfoI +JXULgVweidZhlD0JJK2RGK2K7CVGTPluX07xO6vbAoGAPOPE0oF8sHNxuubQWqYu +JptQUFpAAbNN+RJMf/LVQVxcYHSmBvqVeVjdXYnpi9fuXWNj6mWIUmffvCH89MFf +wMbt5DM2cGlYbh/yiE5Pj9+D6KI9nuR7bbnFfeF9iJnx13kw+JcxOKVSuXbwrYdR +qyRqPvSTtB3nAq1jev7khwECgYBEgldHZicL4jpDu+LVV3/P9ZWFCdQ2bvz4Jpnv +hc+xCisu3O7Htr7m03W3ygHveTR2OcqOoW0rYrF0EgZVmWlZSMzI61oYFn001ia6 +OsvSDqj2fCxQ1IoGTVgAjrEdm85Yh9HauWmW0NxVYxWOBY+Cr5NIEfAjrEZkN0qz +8BNbdQKBgD4w2xm7jFMUgPzHp7L8RWMWLUTBudc981dOPQJ5kAR5n2oEhE1YJs+e +GjJuyhAhz5VdHn2H2+RptQ70RVM+ctDNKYZko2aH4uGZq/6X5MWGr1erLMgMbg5q ++oSLpOUiUobapGdl9fgHetyFw/N9TI1tl/4+2uFqW5knBQnXByPP +-----END RSA PRIVATE KEY-----` + +var rsaPrivateKeyPKCS8 = `-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP7abKDTHtqGkk +6c/jxbZph17QcVz3NxcRrQ8RCLWHZd020oANIssGgZwGuy9hvQUfEYRy1+78wJmV +c7naeJ8qkLj1u0OsDLwofRaYXzkZUFitZr2Ygkzhy8/GVhdIMVnAV2u4LHvpw+dS +8hsnpWnIzF5Rdo3e7KNZbjZlCLBDrmorGsdvYKqwN/7aBd81YaS5dz67oacG0/bI +Bn2ox93OI+OQLrdtYG2aDMv9eEs8QQ8X10YI2Fsp2t2rAstwBGhsSbMPdBF82G9G +XIng4ZTO6P0G1ypYcXha4okhLO2ck15bYyd+EAY3QfyJ5MMcHMvr/iJpGCVeIyFm +m9qoyGqLAgMBAAECggEAdtocLYBvWq6aM1xm1YaNJzMW0kUKY9EcoaDvbMgyo0tp +sE2QnnGV5Ykue3aBtfeKtuCXeeHOHLGm2JPG14d9S6Jf5y58lxrMbsRZpw0/ISYZ +Gj0RANzyP1r10CQjuMNkzxnpW+QpjEzLrFDxjq7xkbKn8x62J4fSM2tZMlVOE9DV +1Mc45/1r3VgEdzkONSBykT51woTdcovUnP4gEg+REky1Wb1S1rk8m1MRAIq4T1Yu +cRyqpNNYhJbXofPwNMhrdo9fqhaCYTrxf8ZpiFDnZHqF28zQtSUm0YgFJR4vZkAd +esBWo++FVefIL3T6VkbOHKN4I4dk+EWlERHjVQz+uQKBgQDrebFo6qoAqr1O/c7d +CDTU4FXZcml7IPSLL3U196WB/MfsP+UVzgD4+DOHenQwHbbj7ta/rgF7iIHliqBX +WdGFgywPs7sNhq11av5ZEAXHD7r8eZBjKlV8IsvMA51MCp1/SK8McxVhBVEJgsGL +VSRxvRz9tVR7wlKcg7DE0aBF3wKBgQDiDUjUNU2HDDmYuCuEmsjH/c6f/P+BjLXp +LnKW0aUbvQl/nDTMTJTIu0zG0+OJhL4GWDkB9DW115kxCGFmZMvrk3LeDqg1QWDQ +d3cxgEdSSsRWBsiABvIn7Fno/MN2NrZd8Wdfk7HIIF0rGOy9ja5/PVl0FxUt4O1X +dRmQ3oq41QKBgHoD4djyl8qmrleLDrDburx/zhxRu7SQnAavPbYML9fOSy3w4dzN +lRVtTw4pdqEkFIvBS8eg+6WuU1jE31bD9NyQ3rj4MbnNin4oRcmSktvWG9cNirLH +0en0AdQiH1Syv2+gEwyJaY+PeLFL7swq/ypsiuQwHKnQRIxTdLpXwQvTAoGAS7+Z +3QpzjUKKdmOYqZnYmDOzrqbv07CcMKRQ37smsbHZ4fotMxyiatVgt+u+/pENwECF +8eKssN+rROQDB3XVY36IamLM+POMhq7RsTPEMo49Vnp1a3loYfpwcoNo2E8jMz22 +ny91zpMRxWRXyHkWtSqQtDcb8MDDp5/kzkfUgnUCgYEAv8CVWPKTuw83/nnqZg26 +URXJ/C7hN/1uU21BuyCTMV/fLiSAsV0ucDV2spqCl3VAXcsECavERVppluVylBcR +DFa6BZS0N0x374JRidFWV0a+Mz7pTqC0TO/M3+y6yaDd766J3bkdh2sq8pnhAnXc +qPYXB5U6tdTrexzaYBKr4gQ= +-----END PRIVATE KEY-----` + +var keyPemEC = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIIrYSSNQFaA2Hwf1duRSxKtLYX5CB04fSeQ6tF1aY/PuoAoGCCqGSM49 +AwEHoUQDQgAEPR3tU2Fta9ktY+6P9G0cWO+0kETA6SFs38GecTyudlHz6xvCdz8q +EKTcWGekdmdDPsHloRNtsiCa697B2O9IFA== +-----END EC PRIVATE KEY-----` + +var keyEd25519 = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +` + +var partiallyValidPEMString = ` +something else +-----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +something else +-----BEGIN CERTIFICATE----- +MIIBcDCCARagAwIBAgIJAMZmuGSIfvgzMAoGCCqGSM49BAMCMBMxETAPBgNVBAMM +CHdoYXRldmVyMB4XDTE4MDgxMDE0Mjg1NFoXDTE4MDkwOTE0Mjg1NFowEzERMA8G +A1UEAwwId2hhdGV2ZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATPwn3WCEXL +mjp/bFniDwuwsfu7bASlPae2PyWhqGeWwe23Xlyx+tSqxlkXYe4pZ23BkAAscpGj +yn5gXHExyDlKo1MwUTAdBgNVHQ4EFgQUElRjSoVgKjUqY5AXz2o74cLzzS8wHwYD +VR0jBBgwFoAUElRjSoVgKjUqY5AXz2o74cLzzS8wDwYDVR0TAQH/BAUwAwEB/zAK +BggqhkjOPQQDAgNIADBFAiEA4yQ/88ZrUX68c6kOe9G11u8NUaUzd8pLOtkKhniN +OHoCIHmNX37JOqTcTzGn2u9+c8NlnvZ0uDvsd1BmKPaUmjmm +-----END CERTIFICATE----- +something else +-----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIJHG93jlLLLTF6Stky5+8Q7mMpgCkYYTO12NDAzlJn3w +-----END PRIVATE KEY----- +something else +` +var invalidData = `nothingtoseehere` + +func TestX509ParseAndVerify(t *testing.T) { + t.Run("TestFullChainPEM", func(t *testing.T) { chain := strings.Join([]string{rootCA, intermediateCA, leaf}, "\n") @@ -111,101 +207,6 @@ nwy7dzejHmQUcZ/aUNbc4VTbiv15ESk= }) } -func TestParseRSAPrivateKey(t *testing.T) { - rsaPrivateKey := `-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA3Y8cXdK06ufUSP035jiwJk8IsuwGjJD/LSRvE2AhJL/Vp9mu -41z1bV5Mi/TTK/uZNqv6VdvTxFPZOUYycLXEchg8L6wrOLgAX0DleP+YTKGG4oyg -dTZZcqzwr4p7WhYzLFmpW8RCLgHJbV0fF1pejJKtV+9fpsdX8oQzKvqO39ne1hl+ -m/lq2LKBK0z03c4ay+bFzA8AFMndmzfB3uXl2fTFsNaoYxAkGwlcvFAXNegPKtaf -9Co5JpRlRejPYVSonCvCvBakGIDCRb0ZHQrcGBzDnqjZeZMDkfe0YKoRUR+JFn69 -C7a4tHheA0TerIDcv+IqadY7p2jwIom9di1oWwIDAQABAoIBAQDXEXGGvd+y20Gd -bHhTuZl8RmH6VNTypFmf92r/UuQ5aSI8Ijn7KKRw+wWxIgHPAxcyE/UYXSCOxpnp -V/Pkpv0/h7j8ydLW5v4teLCIKQws7ushhULJJO3lPG0S6Yld5IjeN1cH5lYblM5z -o95na+i16jfsUUf3fDAqERweT0Rbk7IlegTgXtXLjbvGpFWgjH7Oc8UPpy56i05h -NtdBvQhFV8LMckQAfEinBTPDHqZw6hGIfJtieRhwTzGh5H0fnDCRZanRKm2uxh4Z -9ciYZ/wa0Af23atGoax1YbQJFJK8h0vWcL1jJkaZ+CmVmRtYcWPTpDNGe2FQn9I2 -EwF5nB8BAoGBAPpAsZiFC00YJf1gN4G588+7hxMU2BaoTosImSD27sLLmE2XHBa+ -FrtLJR+t6pRtt7aQccGrNp2G234ucjitM2A1JmtzywPhtAXp+/VaguikdJ62zAjl -Sn6nl9W6ovOQ0NsHGmO7MFILrWXXpF7IqhXd/MdwMnxJABsKqZpBLB2BAoGBAOKl -uARPETauBRdQisEzHI1kosHigCVCSTwwTnFa8LXfinfFCq68SuuwqUdN5RaNUpGx -zTFxOgihcSlfOF0/VXROi6PI768pp2SOgbKjXsleZqxaSe5iZ61jt0uU0HlUsfoI -JXULgVweidZhlD0JJK2RGK2K7CVGTPluX07xO6vbAoGAPOPE0oF8sHNxuubQWqYu -JptQUFpAAbNN+RJMf/LVQVxcYHSmBvqVeVjdXYnpi9fuXWNj6mWIUmffvCH89MFf -wMbt5DM2cGlYbh/yiE5Pj9+D6KI9nuR7bbnFfeF9iJnx13kw+JcxOKVSuXbwrYdR -qyRqPvSTtB3nAq1jev7khwECgYBEgldHZicL4jpDu+LVV3/P9ZWFCdQ2bvz4Jpnv -hc+xCisu3O7Htr7m03W3ygHveTR2OcqOoW0rYrF0EgZVmWlZSMzI61oYFn001ia6 -OsvSDqj2fCxQ1IoGTVgAjrEdm85Yh9HauWmW0NxVYxWOBY+Cr5NIEfAjrEZkN0qz -8BNbdQKBgD4w2xm7jFMUgPzHp7L8RWMWLUTBudc981dOPQJ5kAR5n2oEhE1YJs+e -GjJuyhAhz5VdHn2H2+RptQ70RVM+ctDNKYZko2aH4uGZq/6X5MWGr1erLMgMbg5q -+oSLpOUiUobapGdl9fgHetyFw/N9TI1tl/4+2uFqW5knBQnXByPP ------END RSA PRIVATE KEY-----` - - rsaPrivateKeyPKCS8 := `-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDP7abKDTHtqGkk -6c/jxbZph17QcVz3NxcRrQ8RCLWHZd020oANIssGgZwGuy9hvQUfEYRy1+78wJmV -c7naeJ8qkLj1u0OsDLwofRaYXzkZUFitZr2Ygkzhy8/GVhdIMVnAV2u4LHvpw+dS -8hsnpWnIzF5Rdo3e7KNZbjZlCLBDrmorGsdvYKqwN/7aBd81YaS5dz67oacG0/bI -Bn2ox93OI+OQLrdtYG2aDMv9eEs8QQ8X10YI2Fsp2t2rAstwBGhsSbMPdBF82G9G -XIng4ZTO6P0G1ypYcXha4okhLO2ck15bYyd+EAY3QfyJ5MMcHMvr/iJpGCVeIyFm -m9qoyGqLAgMBAAECggEAdtocLYBvWq6aM1xm1YaNJzMW0kUKY9EcoaDvbMgyo0tp -sE2QnnGV5Ykue3aBtfeKtuCXeeHOHLGm2JPG14d9S6Jf5y58lxrMbsRZpw0/ISYZ -Gj0RANzyP1r10CQjuMNkzxnpW+QpjEzLrFDxjq7xkbKn8x62J4fSM2tZMlVOE9DV -1Mc45/1r3VgEdzkONSBykT51woTdcovUnP4gEg+REky1Wb1S1rk8m1MRAIq4T1Yu -cRyqpNNYhJbXofPwNMhrdo9fqhaCYTrxf8ZpiFDnZHqF28zQtSUm0YgFJR4vZkAd -esBWo++FVefIL3T6VkbOHKN4I4dk+EWlERHjVQz+uQKBgQDrebFo6qoAqr1O/c7d -CDTU4FXZcml7IPSLL3U196WB/MfsP+UVzgD4+DOHenQwHbbj7ta/rgF7iIHliqBX -WdGFgywPs7sNhq11av5ZEAXHD7r8eZBjKlV8IsvMA51MCp1/SK8McxVhBVEJgsGL -VSRxvRz9tVR7wlKcg7DE0aBF3wKBgQDiDUjUNU2HDDmYuCuEmsjH/c6f/P+BjLXp -LnKW0aUbvQl/nDTMTJTIu0zG0+OJhL4GWDkB9DW115kxCGFmZMvrk3LeDqg1QWDQ -d3cxgEdSSsRWBsiABvIn7Fno/MN2NrZd8Wdfk7HIIF0rGOy9ja5/PVl0FxUt4O1X -dRmQ3oq41QKBgHoD4djyl8qmrleLDrDburx/zhxRu7SQnAavPbYML9fOSy3w4dzN -lRVtTw4pdqEkFIvBS8eg+6WuU1jE31bD9NyQ3rj4MbnNin4oRcmSktvWG9cNirLH -0en0AdQiH1Syv2+gEwyJaY+PeLFL7swq/ypsiuQwHKnQRIxTdLpXwQvTAoGAS7+Z -3QpzjUKKdmOYqZnYmDOzrqbv07CcMKRQ37smsbHZ4fotMxyiatVgt+u+/pENwECF -8eKssN+rROQDB3XVY36IamLM+POMhq7RsTPEMo49Vnp1a3loYfpwcoNo2E8jMz22 -ny91zpMRxWRXyHkWtSqQtDcb8MDDp5/kzkfUgnUCgYEAv8CVWPKTuw83/nnqZg26 -URXJ/C7hN/1uU21BuyCTMV/fLiSAsV0ucDV2spqCl3VAXcsECavERVppluVylBcR -DFa6BZS0N0x374JRidFWV0a+Mz7pTqC0TO/M3+y6yaDd766J3bkdh2sq8pnhAnXc -qPYXB5U6tdTrexzaYBKr4gQ= ------END PRIVATE KEY-----` - - t.Run("TestParseRSAPrivateKey", func(t *testing.T) { - parsed, err := getRSAPrivateKeyFromString(rsaPrivateKey) - if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) - } - - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key failed when it was expected to succeed, got %v", err) - } - }) - - t.Run("TestParseRSAPrivateKeyBase64", func(t *testing.T) { - b64 := base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey)) - - parsed, err := getRSAPrivateKeyFromString(b64) - if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) - } - - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key (base64) failed when it was expected to succeed, got %v", err) - } - }) - - t.Run("TestParseRSAPrivateKeyPKCS8", func(t *testing.T) { - parsed, err := getRSAPrivateKeyFromString(rsaPrivateKeyPKCS8) - if err != nil { - t.Fatalf("failed to parse PEM cert: %v", err) - } - - if _, err := jwk.New(parsed); err != nil { - t.Errorf("RSA private key (PKCS8) failed when it was expected to succeed, got %v", err) - } - }) - -} - func Test_parsex509KeyPair(t *testing.T) { certPemEC := []byte(`-----BEGIN CERTIFICATE----- MIIBhTCCASugAwIBAgIQIRi6zePL6mKjOipn+dNuaTAKBggqhkjOPQQDAjASMRAw @@ -465,3 +466,184 @@ KcZjiyUsFLvdC5de1MeT1rJjQEsiZxH+QPR88tuByUVG000lpA== }) } + +func Test_getPrivateKeyFromPEMData(t *testing.T) { + tests := map[string]struct { + input string + wantErr string + keyCheck func(t *testing.T, keys []crypto.PrivateKey) + }{ + "invalid data": { + input: invalidData, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + "rsa key": { + input: rsaPrivateKey, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "base64 rsa key": { + input: base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "rsa key pkcs8": { + input: rsaPrivateKeyPKCS8, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "base64 rsa key pkcs8": { + input: base64.StdEncoding.EncodeToString([]byte(rsaPrivateKeyPKCS8)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "ec key": { + input: keyPemEC, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "base64 ec key": { + input: base64.StdEncoding.EncodeToString([]byte(keyPemEC)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "ed key": { + input: keyEd25519, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "base64 ed key": { + input: base64.StdEncoding.EncodeToString([]byte(keyEd25519)), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "other PEM data, no keys": { + input: fmt.Sprintf("%s\n%s\n%s\n", rootCA, intermediateCA, leaf), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + "partially valid PEM data": { + input: partiallyValidPEMString, + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 2 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + if _, ok := keys[1].(ed25519.PrivateKey); !ok { + t.Fatalf("expected ed25519 key but got %T", keys[0]) + } + }, + }, + "mixed PEM data": { + input: fmt.Sprintf("%s\n%s\n%s\n%s", rootCA, intermediateCA, leaf, keyPemEC), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 1 { + t.Fatalf("expected 1 key but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + }, + }, + "mixed PEM data, two keys": { + input: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", rootCA, intermediateCA, leaf, keyPemEC, rsaPrivateKey), + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 2 { + t.Fatalf("expected 2 keys but got %d", len(keys)) + } + if _, ok := keys[0].(*ecdsa.PrivateKey); !ok { + t.Fatalf("expected ecdsa key but got %T", keys[0]) + } + if _, ok := keys[1].(*rsa.PrivateKey); !ok { + t.Fatalf("expected rsa key but got %T", keys[0]) + } + }, + }, + "corrupted key": { + input: `-----BEGIN PRIVATE KEY----- +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +-----END PRIVATE KEY----- +`, + wantErr: "asn1: structure error", + keyCheck: func(t *testing.T, keys []crypto.PrivateKey) { + if len(keys) != 0 { + t.Fatalf("expected no keys but got %d", len(keys)) + } + }, + }, + } + for name, testData := range tests { + t.Run(name, func(t *testing.T) { + keys, err := getPrivateKeysFromPEMData(testData.input) + if testData.wantErr != "" { + if err != nil && !strings.Contains(err.Error(), testData.wantErr) { + t.Fatalf("got error: %v, want error: %v", err, testData.wantErr) + } else if err == nil { + t.Fatalf("expected error: %v", testData.wantErr) + } + } else { + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + testData.keyCheck(t, keys) + }) + } +}