Skip to content

Commit

Permalink
built-ins: decode a RSA private key into JWK format (#3783)
Browse files Browse the repository at this point in the history
This will help users for JWT signing using RSA key, because currently
OPA only accepts RSA key in the JWK format.

Fixes: #3765

Signed-off-by: cris-he <cruztiempo@hotmail.com>
  • Loading branch information
cris-he authored Sep 17, 2021
1 parent dd9fed3 commit 22d505f
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 2 deletions.
11 changes: 11 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ var DefaultBuiltins = [...]*Builtin{
CryptoSha1,
CryptoSha256,
CryptoX509ParseCertificateRequest,
CryptoX509ParseRSAPrivateKey,

// Graphs
WalkBuiltin,
Expand Down Expand Up @@ -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",
Expand Down
22 changes: 22 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
1 change: 1 addition & 0 deletions docs/content/policy-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ Note that the opa executable will need access to the timezone files in the envir
| <span class="opa-keep-it-together">``output := crypto.x509.parse_certificates(certs)``</span> | ``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`` |
| <span class="opa-keep-it-together">``output := crypto.x509.parse_and_verify_certificates(certs)``</span> | ``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`` |
| <span class="opa-keep-it-together">``output := crypto.x509.parse_certificate_request(csr)``</span> | ``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`` |
| <span class="opa-keep-it-together">``output := crypto.x509.parse_rsa_private_key(pem)``</span> | ``pem`` is a base64 string containing a PEM encoded RSA private key.``output`` is a JWK as a JSON object. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``output := crypto.md5(string)``</span> | ``output`` is ``string`` md5 hashed. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``output := crypto.sha1(string)``</span> | ``output`` is ``string`` sha1 hashed. | ``SDK-dependent`` |
| <span class="opa-keep-it-together">``output := crypto.sha256(string)``</span> | ``output`` is ``string`` sha256 hashed. | ``SDK-dependent`` |
Expand Down
1 change: 1 addition & 0 deletions internal/jwx/jwk/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
34 changes: 32 additions & 2 deletions internal/jwx/jwk/rsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jwk

import (
"crypto/rsa"
"encoding/binary"
"math/big"

"github.com/pkg/errors"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -99,5 +128,6 @@ func (k *RSAPrivateKey) GenerateKey(keyJSON *RawKeyJSON) error {

k.key = privateKey
k.StandardHeaders = &keyJSON.StandardHeaders
k.AlgorithmParameters = &keyJSON.AlgorithmParameters
return nil
}
84 changes: 84 additions & 0 deletions topdown/crypto.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
97 changes: 97 additions & 0 deletions topdown/crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/base64"
"strings"
"testing"

"github.com/open-policy-agent/opa/internal/jwx/jwk"
)

func TestX509ParseAndVerify(t *testing.T) {
Expand Down Expand Up @@ -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)
}
})

}

0 comments on commit 22d505f

Please sign in to comment.