Parse certificate chain CA Issuer#3985
Conversation
a valid flat chain. Returns a chain and optional CA Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
vault Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
runtime Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: JoshVanL The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
flake |
SgtCoDFish
left a comment
There was a problem hiding this comment.
A few suggestions and questions from me; I think it was a great idea to split the original PR up into separate ones 👍
| Subject: pkix.Name{ | ||
| CommonName: "root", | ||
| }, | ||
| PublicKeyAlgorithm: x509.RSA, |
There was a problem hiding this comment.
suggestion: should this be EC not RSA, based on rootPK above?
There was a problem hiding this comment.
(actually, does this need to be specified in the template? I'd imagine it would be filled in automatically when the cert is signed, although I've not tested that)
| "github.com/jetstack/cert-manager/pkg/controller" | ||
| "github.com/jetstack/cert-manager/pkg/controller/certificaterequests" | ||
| "github.com/jetstack/cert-manager/pkg/controller/certificaterequests/util" | ||
| controllertest "github.com/jetstack/cert-manager/pkg/controller/test" |
There was a problem hiding this comment.
suggestion: is this not a re-import duplication of the line below it?
| Subject: pkix.Name{ | ||
| CommonName: name, | ||
| }, | ||
| PublicKeyAlgorithm: x509.RSA, |
There was a problem hiding this comment.
suggestion: we're passed a crypto.Signer here; is it safe to just pass RSA for the key algo? (as before, is setting this field needed at all?)
|
|
||
| // Build root RSA CA | ||
| skRSA, err := pki.GenerateRSAPrivateKey(2048) | ||
| rootPK, err := pki.GenerateRSAPrivateKey(2048) |
There was a problem hiding this comment.
suggestion: could we use P-256 ECDSA keys here instead if we're refactoring the variable names?
| if err != nil { | ||
| t.Error(err) | ||
| t.FailNow() | ||
| t.Fatal() |
There was a problem hiding this comment.
suggestion: would it be better to include the error when we call t.Fatal()?
| t.Fatal() | |
| t.Fatal(err) |
| root := mustCreateBundle(t, nil, "root") | ||
| int1 := mustCreateBundle(t, root, "int-1") | ||
| int2 := mustCreateBundle(t, int1, "int-2") | ||
| leaf := mustCreateBundle(t, int2, "leaf") | ||
| random := mustCreateBundle(t, nil, "random") |
There was a problem hiding this comment.
suggestion: would it be worth adding a test for a different branch off the same root?
e.g. using some random diagram I found, we're currently testing A->B->E->H which is good. Could we also test that ParseCertificateChain works correctly with an input set of, say, {A, B, C, E, G} which should produce A->C->G?
There was a problem hiding this comment.
I may have misunderstood- I thought that this scenario would error- if the input was {A, B, C, E, G} from the diagram, we would end up with a list of chain nodes {A, B, E} and {C, G} that cannot be merged and would error here?
There was a problem hiding this comment.
(caveat: I didn't trace through the algorithm in depth with {A, B, C, E, G}, because ultimately it'll be unit tests that check this stuff and not humans 😁 )
I guess what I'm saying is that A->C->G is a valid chain, and the tree as presented in that diagram is a totally valid PKI hierarchy/tree which could be passed to the function, but we don't have a test which checks its operation.
It's reasonable IMO to do any of the following:
- if we can construct two (partial or complete) chains to the same root from given input, we'll error (so
{A, B, C, E, G}fails because there'sA->C->GandA->B->E) - or if we can construct two full chains from root to leaf we'll error (so
{A, B, C, E, G, H}fails because there'sA->B->E->HandA->C->Gbut{A, B, C, E, G}succeeds withA->C->GsinceA->B->Edoesn't end in a leaf) - or we return all chains we can construct (so
{A, B, C, E, G}returns{{A->C->G}, {A->B->E},{A->B},{A->C}} - or we return all complete chains we can construct (so
{A, B, C, E, G}returns{{A->C->G}}and{A, B, C, E, G, H}returns{{A->B->E->H}, {A->C->G}})
I think I'd prefer 2 as an implementation, but this is quite academic because in most cases we'd only ever be passed, say {A, A, B, B, E, H} which the algorithm should handle fine.
Whichever approach we take, I think my main point is that it would be super valuable to have a test case which takes an input of {A, B, C, E, G} and one which takes input of {A, B, C, E, G, H}; if they both end up erroring that's fine.
There was a problem hiding this comment.
Thanks for detailed explanation. Indeed having multiple branches in the same bundle is totally valid and is likely wanted for services with multiple intermediates; they may not even share the same roots, but do trust them. The intention here though is that we expect a single chain when a certificate is signed, i.e. the issuer returns the single chain from root to the new signed certificate.
I think managing these larger bundle trees is a separate concern, and falls under the "trust distribution" planned work (whatever that may look like). Given this, I would like to continue with 1 as the implementation. In future it may be that other functions are made to cover those other cases.
To make it clear this is the intention of this function and wouldn't be appropriate for other cases, I'm going to rename it to ParseSingleCertificateChain. Open to suggestions for better names 😄
There was a problem hiding this comment.
I'll also add some tests to demonstrate that this function should fail with branching chains.
pkg/util/pki/parse_test.go
Outdated
| bundle, err := ParseCertificateChainPEM(test.inputBundle) | ||
| if (err != nil) != test.expErr { | ||
| t.Errorf("unexpected error, exp=%t got=%v", | ||
| test.expErr, err) | ||
| } |
There was a problem hiding this comment.
question: do you think we're doing enough testing that the whitespace is correct in the output PEM we expect?
I think we're relying on the PEM output from SignCertificate in mustCreateBundle having the required \n at the end; that's probably not likely to change, but given that ParseCertificateChain is so important, would it be good to make sure in these tests that if we pass a cert with no newline at the end it's handled correctly and we don't end up with a line like:
-----END CERTIFICATE----------BEGIN CERTIFICATE-----
There was a problem hiding this comment.
Hmm, I don't think this is too much of a concern. Considering we do all the checks I think this should be fine.
pkg/util/pki/parse.go
Outdated
| continue | ||
| } | ||
|
|
||
| // attempt to add both chain together |
There was a problem hiding this comment.
nitpick: spelling
| // attempt to add both chain together | |
| // attempt to add both chains together |
| } | ||
|
|
||
| // attempt to add both chain together | ||
| chain, ok := chains[i].tryMergeChain(chains[j]) |
There was a problem hiding this comment.
question: especially with larger RSA key sizes (4096 being common for roots, which we'll be using here) checking signatures can be quite an expensive operation, and we're potentially going to be doing it a lot in tryMergeChain for larger chains. [1]
we can maybe remove the need to do so many signature checks by ordering the nodes differently before we check signatures; we have access to the issuer and subject for every cert in the chain, and in most cases (i.e. when the chain is sane) we'll be able to order certs based on their issuer DN. we still need to check the signatures for correctness reasons, but ordering based on the DNs will save a lot of signature checks.
do you think this optimization is worth it now? I don't think it's needed for this PR (although we need to be careful of a potential performance regression here with longer chains). I could look at adding it in the future if you don't think it's required here.
[1] looking way into the future, quantum-resistant TLS will likely be even more computationally expensive.
There was a problem hiding this comment.
Yep, I think it is worth while thinking about and keeping an eye on. I'm not keen on pre optimising though given we don't know if this will be the limiting factor.
There was a problem hiding this comment.
yeah totally reasonable, I'm sure this will perform fine as implemented 👍
pkg/util/pki/parse.go
Outdated
| // | ||
| // An error is returned if the passed bundle is not a valid flat tree chain, | ||
| // the bundle is malformed, or the chain is broken. | ||
| func ParseCertificateChain(certs []*x509.Certificate) (PEMBundle, error) { |
There was a problem hiding this comment.
praise: I think this will clean up a lot of stuff, and it seems super valuable to me.
irbekrm
left a comment
There was a problem hiding this comment.
Amazing code, I wish we did work more work like this!
I have added a comment where I think we should not modify the slice inside the for loop.
intention better Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
target Secret Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
certificate down Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
Signed-off-by: joshvanl <vleeuwenjoshua@gmail.com>
c819d09 to
58a2531
Compare
|
/retest |
|
/lgtm |
|
/hold |
|
/unhold |

This PR is branched from #3982 and should be merged first. EDIT: This should say "and that PR should be merged first" instead of "and should be merged first"
Updates
SignCSRTemplateto use newParseCertificateChainfunc, and passes through CA certificate when chaining CA Issuers together./assign @maelvls @munnerz @SgtCoDFish @jakexks