Skip to content

Conversation

meyskens
Copy link
Contributor

@meyskens meyskens commented Aug 21, 2020

What this PR does / why we need it:

The ACME API now allows for alternate certificates being fetched signed by different CAs. Let's Encrypt does this by serving both their cross signed and own CA root certificates. We currently do not support specifying a preference for one.
certbot recently added support for this with the upcoming switch to the ISRG CA, they added:

  --preferred-chain PREFERRED_CHAIN
                        If the CA offers multiple certificate chains, prefer
                        the chain with an issuer matching this Subject Common
                        Name. If no match, the default offered chain will be

This PR adds similar behavior to cert-manager, while I didn't look at the exact checks it does and error handling it seems I wrote the exact dame thing 😺

Which issue this PR fixes:
Fixes #1700

Special notes for your reviewer:

  • Please also look at my fork of the Go library, we should make an upstream PR just like we did with EAB (not yet merged...)

  • Needs unit and e2e tests, thus WIP

Release note:

Add support for alternate certs with `prefferedChain` in ACME

@jetstack-bot jetstack-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates that all commits in the pull request have the valid DCO sign-off message. needs-kind Indicates a PR lacks a `kind/foo` label and requires one. size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Aug 21, 2020
@jetstack-bot jetstack-bot requested a review from munnerz August 21, 2020 15:46
@jetstack-bot jetstack-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 21, 2020
@meyskens
Copy link
Contributor Author

ooops... i didn't commit any code

@meyskens
Copy link
Contributor Author

Fixed!

@meyskens meyskens changed the title Add support for alternate certs with PrefferedChain in ACME WIP Add support for alternate certs with PrefferedChain in ACME Aug 21, 2020
@jetstack-bot jetstack-bot added the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 21, 2020
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
@jetstack-bot jetstack-bot added area/acme Indicates a PR directly modifies the ACME Issuer code area/api Indicates a PR directly modifies the 'pkg/apis' directory area/deploy Indicates a PR modifies deployment configuration size/L Denotes a PR that changes 100-499 lines, ignoring generated files. and removed size/XS Denotes a PR that changes 0-9 lines, ignoring generated files. labels Aug 21, 2020
@meyskens meyskens added this to the v1.0 milestone Aug 24, 2020
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
@meyskens meyskens changed the title WIP Add support for alternate certs with PrefferedChain in ACME Add support for alternate certs with PrefferedChain in ACME Aug 24, 2020
@jetstack-bot jetstack-bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 24, 2020
@meyskens
Copy link
Contributor Author

Good e2e test seems a bit too hard as Pebble sets random CA names. Would be good to have this somehow able to do those in the future but a bit out of scope for this PR. So I added a unit test instead

@meyskens
Copy link
Contributor Author

/kind feature

@jetstack-bot jetstack-bot added kind/feature Categorizes issue or PR as related to a new feature. and removed needs-kind Indicates a PR lacks a `kind/foo` label and requires one. labels Aug 24, 2020
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
@meyskens
Copy link
Contributor Author

/retest

Copy link
Member

@wallrj wallrj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you're still working on this, so sorry for the premature review, but a few comments anyway, particularly regarding the crypto fork and the go.mod replace.

@@ -4,7 +4,7 @@ go 1.12

replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v0.9.4

replace golang.org/x/crypto => github.com/munnerz/crypto v0.0.0-20191203200931-e1844778daa5
replace golang.org/x/crypto => github.com/meyskens/crypto v0.0.0-20200821143559-6ca9aec645f0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry to be a pain, but it makes me nervous to replace this stdlib crypto library with a fork.
But if we must do it, I think:

  • the fork should live in the cert-manager organization on Github
  • the reason for the fork should be explained briefly here, and in more detail in the fork's README.
  • make sure that the fork does not fall behind the upstream repo.
  • all changes should have links to corresponding upstream issues and pull requests so that upstream can give feedback on our proposed changes.
  • Ensure that we do thorough code review and testing of our changes in the fork.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fork should live in the cert-manager organization on Github

Agreed, this should have been done when we first forked this, i don't think this fork was meant to stay for long but unfortunately we have had no feedback yet on our EAB PR.

the reason for the fork should be explained briefly here, and in more detail in the fork's README.

Disagree, this causes a commit that makes it harder to sync between upstream and commit back to upstream, the fork isn't meant to stay once merged into upstream. We should add a note in the GitHub description however

make sure that the fork does not fall behind the upstream repo.

Sure thing

all changes should have links to corresponding upstream issues and pull requests so that upstream can give feedback on our proposed changes.

EAB already has this done, this change will be PRed upstream as soon as it is reviewed by our team.

Ensure that we do thorough code review and testing of our changes in the fork.

See reviewers note ;)

// if no match is found we return to the actual cert
// it is a *prefered* chain after all
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does FetchCertAlternatives always include the certSlice bundle?
If not, how is a user supposed to prefer the certSlice chain? By omitting the PreferredChain value? That seems unintuitive.

Also, if the user mistypes the PreferredChain, then they get back the default chain, without any warning or errors logged.
I'd be inclined to fail the order if the PreferredChain is not found, but at least log and /or a warning event so that the user can quickly see that the preferred chain is wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does FetchCertAlternatives always include the certSlice bundle?
If not, how is a user supposed to prefer the certSlice chain? By omitting the PreferredChain value? That seems unintuitive.

FetchCertAlternatives gives all alternative chains this can be 0 or 100. PreferredChain picks the one with the correct root issuer. If not present it will use the "normal" one this allows the ACME server to be free to select which chain it offers in the non alternative reply

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

cert, err := x509.ParseCertificate(certPEM)
if err != nil {
return fmt.Errorf("error parsing alternate certificates: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this return the first or the last certificate in a multi-pem bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it returns all certs in the multi-pem bundle which were in the bundle of multi multi-pem bundles... gets confusing but that's how it works

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I didn't read the code properly. Didn't see the nested for loop.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered if there was a helper function that could take a slice of certificates and construct a chain, with a leaf and a "root" (where root is the root of this chain rather than a global root cert).
It would return an error if the certificates in the bundle did not form a chain.
Then we could compare the CN to that of the chain root.

// PreferredChain is no guarantee that this one gets delivered by the ACME
// endpoint.
// For example, for Let's Encrypt's DST crosssign you would use:
// "DST Root CA X3" or "ISRG Root X1" for the newer Let's Encrypt root CA.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain that this must match the CommonName of the first (or is it last?) certificate in the chain, where the first certificate is the chain root CA and the last certificate is the leaf certificate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It returns if any cert's issuer in the chain matches it, ACME does not have to (and LE does not) give the root CA cert back only leaf and intermediate so we look at the issuer field, analog to certbot

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. In my mind, it should only match the highest level CA certificate in the bundle and only match a CA certificate which can be chained to the leaf certificate.

Or to put it another way, you should get an error if you happen to enter the CN of the leaf certificate here.
And you should get an error if the bundle contains any certificates which are not involved in a chain of signatures leading to the leaf cert.

l.log.V(logf.TraceLevel).Info("Calling FetchCertAlternatives")

ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems a bit weird to add a timeout in "logging" middleware, but I see that that is done elsewhere, so fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This always has been a weird middleware... never liked it but it has it's reasons to be here

Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
Signed-off-by: Maartje Eyskens <maartje@eyskens.me>
@meyskens meyskens requested a review from wallrj August 26, 2020 08:25
Copy link
Member

@wallrj wallrj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more questions below.
But if you think my suggestions are overkill, or should be deferred for a later release, I'm happy to give a lgtm.

cert, err := x509.ParseCertificate(certPEM)
if err != nil {
return fmt.Errorf("error parsing alternate certificates: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I didn't read the code properly. Didn't see the nested for loop.

// PreferredChain is no guarantee that this one gets delivered by the ACME
// endpoint.
// For example, for Let's Encrypt's DST crosssign you would use:
// "DST Root CA X3" or "ISRG Root X1" for the newer Let's Encrypt root CA.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. In my mind, it should only match the highest level CA certificate in the bundle and only match a CA certificate which can be chained to the leaf certificate.

Or to put it another way, you should get an error if you happen to enter the CN of the leaf certificate here.
And you should get an error if the bundle contains any certificates which are not involved in a chain of signatures leading to the leaf cert.

cert, err := x509.ParseCertificate(certPEM)
if err != nil {
return fmt.Errorf("error parsing alternate certificates: %w", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered if there was a helper function that could take a slice of certificates and construct a chain, with a leaf and a "root" (where root is the root of this chain rather than a global root cert).
It would return an error if the certificates in the bundle did not form a chain.
Then we could compare the CN to that of the chain root.

// if no match is found we return to the actual cert
// it is a *prefered* chain after all
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

Copy link
Member

@wallrj wallrj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed at standup where @meyskens explained that we are checking the issuer common name rather than the subject common name.
This means that the preferredChain value will only match a bundle where there is a certificate issued by that name so it should never match the leaf certifciate.

We can improve this further by performing extra checks to ensure that we only match CA certificates and that we only match the top-level CA certificate in the chain.

I've also created an issue to document this feature #3219

/lgtm

@jetstack-bot jetstack-bot added the lgtm Indicates that a PR is ready to be merged. label Aug 26, 2020
@jetstack-bot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: meyskens, wallrj

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@jetstack-bot jetstack-bot merged commit 6a2f953 into cert-manager:master Aug 26, 2020
@inteon inteon changed the title Add support for alternate certs with PrefferedChain in ACME Add support for alternate certs with PreferredChain in ACME Feb 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. area/acme Indicates a PR directly modifies the ACME Issuer code area/api Indicates a PR directly modifies the 'pkg/apis' directory area/deploy Indicates a PR modifies deployment configuration dco-signoff: yes Indicates that all commits in the pull request have the valid DCO sign-off message. kind/feature Categorizes issue or PR as related to a new feature. lgtm Indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Option to select intermediate certificate
3 participants