diff --git a/ast/builtins.go b/ast/builtins.go index cc69a6a978..c5b47a60c1 100644 --- a/ast/builtins.go +++ b/ast/builtins.go @@ -195,6 +195,7 @@ var DefaultBuiltins = [...]*Builtin{ CryptoSha1, CryptoSha256, CryptoX509ParseCertificateRequest, + CryptoX509ParseRSAPrivateKey, // Graphs WalkBuiltin, @@ -1797,6 +1798,16 @@ var CryptoX509ParseCertificateRequest = &Builtin{ ), } +// CryptoX509ParseRSAPrivateKey returns a JWK for signing a JWT from the given +// PEM-encoded RSA private key. +var CryptoX509ParseRSAPrivateKey = &Builtin{ + Name: "crypto.x509.parse_rsa_private_key", + Decl: types.NewFunction( + types.Args(types.S), + types.NewObject(nil, types.NewDynamicProperty(types.S, types.A)), + ), +} + // CryptoMd5 returns a string representing the input string hashed with the md5 function var CryptoMd5 = &Builtin{ Name: "crypto.md5", diff --git a/capabilities.json b/capabilities.json index 7aec357e8b..76f8c1c97a 100644 --- a/capabilities.json +++ b/capabilities.json @@ -674,6 +674,28 @@ "type": "function" } }, + { + "name": "crypto.x509.parse_rsa_private_key", + "decl": { + "args": [ + { + "type": "string" + } + ], + "result": { + "dynamic": { + "key": { + "type": "string" + }, + "value": { + "type": "any" + } + }, + "type": "object" + }, + "type": "function" + } + }, { "name": "div", "decl": { diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md index 31feb17bed..27828eb6b8 100644 --- a/docs/content/policy-reference.md +++ b/docs/content/policy-reference.md @@ -796,6 +796,7 @@ Note that the opa executable will need access to the timezone files in the envir | ``output := crypto.x509.parse_certificates(certs)`` | ``certs`` is base64 encoded DER or PEM data containing one or more certificates or a PEM string of one or more certificates. ``output`` is an array of X.509 certificates represented as JSON objects. | ``SDK-dependent`` | | ``output := crypto.x509.parse_and_verify_certificates(certs)`` | ``certs`` is base64 encoded DER or PEM data containing two or more certificates where the first is a root CA, the last is a leaf certificate, and all others are intermediate CAs. ``output`` is of the form ``[valid, certs]``. If the input certificate chain could be verified then ``valid`` is ``true`` and ``certs`` is an array of X.509 certificates represented as JSON objects. If the input certificate chain could not be verified then ``valid`` is ``false`` and ``certs`` is ``[]``. | ``SDK-dependent`` | | ``output := crypto.x509.parse_certificate_request(csr)`` | ``csr`` is a base64 string containing either a PEM encoded or DER CSR or a string containing a PEM CSR.``output`` is an X.509 CSR represented as a JSON object. | ``SDK-dependent`` | +| ``output := crypto.x509.parse_rsa_private_key(pem)`` | ``pem`` is a base64 string containing a PEM encoded RSA private key.``output`` is a JWK as a JSON object. | ``SDK-dependent`` | | ``output := crypto.md5(string)`` | ``output`` is ``string`` md5 hashed. | ``SDK-dependent`` | | ``output := crypto.sha1(string)`` | ``output`` is ``string`` sha1 hashed. | ``SDK-dependent`` | | ``output := crypto.sha256(string)`` | ``output`` is ``string`` sha256 hashed. | ``SDK-dependent`` | diff --git a/internal/jwx/jwk/interface.go b/internal/jwx/jwk/interface.go index 9822973052..7a7d03ef1c 100644 --- a/internal/jwx/jwk/interface.go +++ b/internal/jwx/jwk/interface.go @@ -48,6 +48,7 @@ type RSAPublicKey struct { // RSAPrivateKey is a type of JWK generated from RSA private keys type RSAPrivateKey struct { *StandardHeaders + *jwa.AlgorithmParameters key *rsa.PrivateKey } diff --git a/internal/jwx/jwk/rsa.go b/internal/jwx/jwk/rsa.go index c885ffffc1..1a5cba47b6 100644 --- a/internal/jwx/jwk/rsa.go +++ b/internal/jwx/jwk/rsa.go @@ -2,6 +2,7 @@ package jwk import ( "crypto/rsa" + "encoding/binary" "math/big" "github.com/pkg/errors" @@ -29,9 +30,37 @@ func newRSAPrivateKey(key *rsa.PrivateKey) (*RSAPrivateKey, error) { if err != nil { return nil, errors.Wrapf(err, "Failed to set Key Type") } + + var algoParams jwa.AlgorithmParameters + + // it is needed to use raw encoding to omit the "=" paddings at the end + algoParams.D = key.D.Bytes() + algoParams.P = key.Primes[0].Bytes() + algoParams.Q = key.Primes[1].Bytes() + algoParams.Dp = key.Precomputed.Dp.Bytes() + algoParams.Dq = key.Precomputed.Dq.Bytes() + algoParams.Qi = key.Precomputed.Qinv.Bytes() + + // "modulus" (N) from the public key in the private key + algoParams.N = key.PublicKey.N.Bytes() + + // make the E a.k.a "coprime" + // https://en.wikipedia.org/wiki/RSA_(cryptosystem) + coprime := make([]byte, 8) + binary.BigEndian.PutUint64(coprime, uint64(key.PublicKey.E)) + // find the 1st index of non 0x0 paddings from the beginning + i := 0 + for ; i < len(coprime); i++ { + if coprime[i] != 0x0 { + break + } + } + algoParams.E = coprime[i:] + return &RSAPrivateKey{ - StandardHeaders: &hdr, - key: key, + StandardHeaders: &hdr, + AlgorithmParameters: &algoParams, + key: key, }, nil } @@ -99,5 +128,6 @@ func (k *RSAPrivateKey) GenerateKey(keyJSON *RawKeyJSON) error { k.key = privateKey k.StandardHeaders = &keyJSON.StandardHeaders + k.AlgorithmParameters = &keyJSON.AlgorithmParameters return nil } diff --git a/topdown/crypto.go b/topdown/crypto.go index 68b3955abd..502eb26484 100644 --- a/topdown/crypto.go +++ b/topdown/crypto.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/internal/jwx/jwk" "github.com/open-policy-agent/opa/topdown/builtins" "github.com/open-policy-agent/opa/util" ) @@ -30,6 +31,12 @@ const ( // blockTypeCertificateRequest indicates this PEM block contains a certificate // request. Exported for tests. blockTypeCertificateRequest = "CERTIFICATE REQUEST" + // blockTypeRSAPrivateKey indicates this PEM block contains a RSA private key. + // Exported for tests. + blockTypeRSAPrivateKey = "RSA PRIVATE KEY" + // blockTypeRSAPrivateKey indicates this PEM block contains a RSA private key. + // Exported for tests. + blockTypePrivateKey = "PRIVATE KEY" ) func builtinCryptoX509ParseCertificates(a ast.Value) (ast.Value, error) { @@ -126,6 +133,43 @@ func builtinCryptoX509ParseCertificateRequest(a ast.Value) (ast.Value, error) { return ast.InterfaceToValue(x) } +func builtinCryptoX509ParseRSAPrivateKey(_ BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error { + + a := args[0].Value + input, err := builtins.StringOperand(a, 1) + if err != nil { + return err + } + + // get the raw private key + rawKey, err := getRSAPrivateKeyFromString(string(input)) + if err != nil { + return err + } + + rsaPrivateKey, err := jwk.New(rawKey) + if err != nil { + return err + } + + jsonKey, err := json.Marshal(rsaPrivateKey) + if err != nil { + return err + } + + var x interface{} + 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 hashHelper(a ast.Value, h func(ast.String) string) (ast.Value, error) { s, err := builtins.StringOperand(a, 1) if err != nil { @@ -153,6 +197,7 @@ func init() { RegisterFunctionalBuiltin1(ast.CryptoSha1.Name, builtinCryptoSha1) RegisterFunctionalBuiltin1(ast.CryptoSha256.Name, builtinCryptoSha256) RegisterFunctionalBuiltin1(ast.CryptoX509ParseCertificateRequest.Name, builtinCryptoX509ParseCertificateRequest) + RegisterBuiltinFunc(ast.CryptoX509ParseRSAPrivateKey.Name, builtinCryptoX509ParseRSAPrivateKey) } func verifyX509CertificateChain(certs []*x509.Certificate) ([]*x509.Certificate, error) { @@ -226,6 +271,45 @@ 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)) + } + + // assume input is base64 if not PEM + b64, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return nil, err + } + + 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) + +} + // addCACertsFromFile adds CA certificates from filePath into the given pool. // If pool is nil, it creates a new x509.CertPool. pool is returned. func addCACertsFromFile(pool *x509.CertPool, filePath string) (*x509.CertPool, error) { diff --git a/topdown/crypto_test.go b/topdown/crypto_test.go index 6e45f9024d..d2b7cbb9c9 100644 --- a/topdown/crypto_test.go +++ b/topdown/crypto_test.go @@ -4,6 +4,8 @@ import ( "encoding/base64" "strings" "testing" + + "github.com/open-policy-agent/opa/internal/jwx/jwk" ) func TestX509ParseAndVerify(t *testing.T) { @@ -108,3 +110,98 @@ 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) + } + }) + +}