-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
JWK: decode a RSA private key into JWK format. #3783
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
topdown/crypto.go
Outdated
if str := string(input); !strings.HasPrefix(str, "-----BEGIN RSA PRIVATE KEY-----") { | ||
bytes, err = base64.StdEncoding.DecodeString(str) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
// decode the pem into the Block struct | ||
p, _ := pem.Decode(bytes) | ||
if p != nil && p.Type != blockTypeRSAPrivateKey { | ||
return nil, fmt.Errorf("invalid PEM-encoded certificate signing request") | ||
} | ||
if p != nil { | ||
// generally you want your key can hit this following statement, | ||
// because it means your key is in a good shape (parse-able). | ||
bytes = p.Bytes | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this block be simplified ? Something like:
block, _ := pem.Decode([]byte(input))
if block == nil {
return nil, fmt.Errorf("failed to parse PEM block containing the key")
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
// handle error
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the code are now split into 2 new functions (which is similar to the "getX509CertsFromString" and "getX509CertsFromPem" in the "builtinCryptoX509ParseAndVerifyCertificates")
- getRSAPrivateKeyFromString
- getRSAPrivateKeyFromPEM
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got a few nitpicks, please bear with me 🙃
But this looks like an outstanding "first contribution" 👏 thank you
topdown/crypto.go
Outdated
return err | ||
} | ||
|
||
invalid := ast.BooleanTerm(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I think we might better return an error here? That way, the builtin call becomes undefined, and the error will be visible if running with strict-builtin-errors, or when #3742 is implemented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you mean instead of using "ast.BooleanTerm", to use a "ast.StringTerm" with a more cleared error message in the following err condition block?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not exactly, I mean the built-in should fail. The common way to do that is to return an error. If it's not a halt error, evaluation will continue, the call's value is undefined.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, so instead of returning the ast boolean term, return the err directly as "return err" in the following blocks. Will update this shortly.
topdown/crypto.go
Outdated
jsonKey, err := json.Marshal(rsaPrivateKey) | ||
if err != nil { | ||
return iter(invalid) | ||
} | ||
|
||
var x interface{} | ||
if err := util.UnmarshalJSON(jsonKey, &x); err != nil { | ||
return err | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to go through JSON here? I suppose it's a shortcut, we could also declare the object as-is, couldn't we? But I guess this matches what we do when the key is expected in object form (i.e. marshal it to *PrivateKey), so this is probably OK 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it's more like a shortcut, and the following ast.InterfaceToValue()
needs it in that form and I didn't step into very much for the InterfaceToValue
😄
topdown/crypto_test.go
Outdated
t.Run("TestParseRSAPrivateKey", func(t *testing.T) { | ||
parsed, err := getRSAPrivateKeyFromString(rsaPrivateKey) | ||
if err != nil { | ||
t.Fatalf("failed to parse PEM cert chain: %v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 Is this a PEM cert chain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad 🥲
topdown/crypto_test.go
Outdated
} | ||
|
||
if _, err := jwk.New(parsed); err != nil { | ||
t.Error("RSA private key failed when it was expected to succeed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit]
t.Error("RSA private key failed when it was expected to succeed") | |
t.Errorf("RSA private key failed when it was expected to succeed, got %v", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Left a few comments. The one thing I think we might want to consider though is why this is needed in the first place. If this is only about token signing, would it not be better if those methods accepted a PEM encoded key directly? There seems to be an asymmetry in how we currently allow that for verification but not for signing. Or do we expect this to be useful outside of that particular use case?
topdown/crypto.go
Outdated
return nil, fmt.Errorf("PEM block type is '%s', expected %s", p.Type, blockTypeRSAPrivateKey) | ||
} | ||
|
||
return x509.ParsePKCS1PrivateKey(p.Bytes) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about PKCS8? We've had issues with that in the past.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the go lib func ParsePKCS8PrivateKey(der []byte) (key interface{}, err error)
is more for all asymmetric keys but not RSA only, and the func ParsePKCS1PrivateKey(der []byte) (*rsa.PrivateKey, error)
is designed for RSA.
Since this feature is more dedicated for the RSA keys (also function names all prefixed with "RSA"), here the PKCS1 is more suitable, and I think for the PKCS8 we could work in another branch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you are correct that it's not RSA exclusive, RSA is still valid - and I think a built-in named parse_rsa_private_key
needs to take this into account. I think the previously linked issues show that. We already have working code to deal with this, so I'd suggest we reuse that. It should not add much in terms of effort, or what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean, and I was confused that if we also deal with PKCS8 here that I might have to remove the "RSA" prefix in the function name.
I will add the condition block at the end here which will be similar to what in the GetSigningKey
It will be something like this:
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
pkcs8priv, err2 := x509.ParsePKCS8PrivateKey(block.Bytes)
if err2 != nil {
return nil, fmt.Errorf("error parsing private key (%v), (%v)", err, err2)
}
return pkcs8priv, nil
}
return priv, nil
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may have broken a test over here:
opa/internal/jwx/jwk/rsa_test.go
Lines 44 to 46 in 4747e22
if !bytes.Equal(jsonBuf1, jsonBuf2) { | |
t.Fatal("JSON marshal buffers do not match") | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you catching it, and I have pushed the fix.
Thank you all for reviewing! I think this feature would be only used for token signing as far as I can tell, it seems no one or no any other things that requires the RSA in the JWK format with the algo params. I guess in general a "symmetric key" is good enough for most of the cases or companies, but mine here just needs a more strict control on this so we use RSA here. The current parser |
3acd283
to
78fbfe6
Compare
Could anyone help me to understand the Is it generated by a version update script? |
Sorry for the silence here. Yeah, it's |
Right, I wasn't talking about asymmetric keys here but the fact that our signing functions (which do not accept PEM encoded keys) are different from our verification functions (which do accept PEM encoded keys) - they are different from each other in that regard, i.e. asymmetric :) Given how rarely OPA currently issues tokens outside of test environments, I think we can live with that until this changes.. and this additional helper function is certainly helpful. |
48de29e
to
9eba4de
Compare
Thanks, and I have squashed all my commits since I made all updates regarding to each comment, if no more changes please let me know if this PR can be merged. |
1c18080
to
54b7dae
Compare
ℹ️ I've changed something with the required tests, this PR will go green when it's rebased. |
54b7dae
to
11f9fab
Compare
Hi @srenatus, @anderseknert and @ashutosh-narkar, thank you very much for reviewing! I have finished the rebasing and please let me know when it can go green and merged, thanks again! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
This will help users for JWT signing using RSA key, because currently OPA only accepts RSA key in the JWK format. Fixes: open-policy-agent#3765 Signed-off-by: cris-he <cruztiempo@hotmail.com>
11f9fab
to
6d80586
Compare
I think I just lost the "approval" after another rebase to make the branch up-to-date, can anyone help approve this again 🥲 |
Thanks again! 👏 |
Thanks for contributing @cris-he! I'm currently writing a blog post on using OPA to connect to GCP resources. As they require a signed JWT but deliver a PEM in their credentials file, this will be a first good real world test, as well as a spotlight on the new function :) |
Works like a charm, and here's some POC code: https://gist.github.com/anderseknert/0789dd67df717f8d7de86786d002e5b7 |
…t#3783) This will help users for JWT signing using RSA key, because currently OPA only accepts RSA key in the JWK format. Fixes: open-policy-agent#3765 Signed-off-by: cris-he <cruztiempo@hotmail.com> Signed-off-by: Dolev Farhi <farhi.dolev@gmail.com>
Issue: #3765
Currently the JWT sign of the OPA it only accepts the RSA key in the JWK format, this PR adds a decoder that converts the RSA private key into JWK format.
End users could now use
crypto.x509.parse_rsa_private_key
to decode the RSA private key, and thebuiltinCryptoX509ParseRSAPrivateKey
will return the JWK containing the parameters of "n, e, d, p, q, dp, dq, qi" which can be directly used in theio.jwt.encode_sign
Example usage in Rego: