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)
+ }
+ })
+
+}