Skip to content
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

Add SecretPublicKeysDiffersFromCurrentCertificateRequest check #6168

Merged
merged 5 commits into from Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions internal/controller/certificates/policies/checks.go
Expand Up @@ -166,6 +166,35 @@ func SecretIssuerAnnotationsNotUpToDate(input Input) (string, string, bool) {
return "", "", false
}

// SecretPublicKeyDiffersFromCurrentCertificateRequest checks that the current CertificateRequest
// contains a CSR that is signed by the key stored in the Secret. A failure is often caused by the
// Secret being changed outside of the control of cert-manager, causing the current CertificateRequest
// to no longer match what is stored in the Secret.
func SecretPublicKeyDiffersFromCurrentCertificateRequest(input Input) (string, string, bool) {
if input.CurrentRevisionRequest == nil {
return "", "", false
}
pk, err := pki.DecodePrivateKeyBytes(input.Secret.Data[corev1.TLSPrivateKeyKey])
if err != nil {
return InvalidKeyPair, fmt.Sprintf("Issuing certificate as Secret contains invalid private key data: %v", err), true
Copy link
Member

Choose a reason for hiding this comment

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

We discussed this in person yesterday and we agree that the error messages returned from this function should only say what is wrong. They should not say what will happen. That is the job of the code that calls this function.

Other policy check functions also make this mistake, so we should correct all of them in a separate PR.

}

csr, err := pki.DecodeX509CertificateRequestBytes(input.CurrentRevisionRequest.Spec.Request)
if err != nil {
return InvalidCertificateRequest, fmt.Sprintf("Failed to decode current CertificateRequest: %v", err), true
}

equal, err := pki.PublicKeysEqual(csr.PublicKey, pk.Public())
if err != nil {
return InvalidCertificateRequest, fmt.Sprintf("CertificateRequest's public key is invalid: %v", err), true
}
Comment on lines +187 to +190
Copy link
Member

Choose a reason for hiding this comment

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

I think this is a decoding error which not happen under normal circumstances.
If the CSR public key is (somehow) corrupt, then might triggering a re-issuance make things worse?
Won't it cause an endless loop of new, corrupt CSRs?

If you disagree, please add a comment to explain why this return value is appropriate in this case.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think the CSR public key would be corrupt. it would be eg. a ecdh.PublicKey or dsa.PublicKey key (corrupt keys will give parsing errors). I don't think we can generate CSRs with such keys, so we should be fine?

if !equal {
return SecretMismatch, "Secret contains a private key that does not match the current CertificateRequest", true
}

return "", "", false
}

func CurrentCertificateRequestNotValidForSpec(input Input) (string, string, bool) {
if input.CurrentRevisionRequest == nil {
// Fallback to comparing the Certificate spec with the issued certificate.
Expand Down
25 changes: 25 additions & 0 deletions internal/controller/certificates/policies/checks_test.go
Expand Up @@ -213,6 +213,31 @@ func Test_NewTriggerPolicyChain(t *testing.T) {
message: "Issuing certificate as Secret was previously issued by IssuerKind.new.example.com/testissuer",
reissue: true,
},
"trigger if the Secret contains a different private key than was used to sign the CSR": {
certificate: &cmapi.Certificate{Spec: cmapi.CertificateSpec{SecretName: "something"}},
secret: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "something"},
Data: map[string][]byte{
corev1.TLSPrivateKeyKey: staticFixedPrivateKey,
corev1.TLSCertKey: testcrypto.MustCreateCert(
t, staticFixedPrivateKey,
&cmapi.Certificate{Spec: cmapi.CertificateSpec{CommonName: "example.com"}},
),
},
},
request: &cmapi.CertificateRequest{Spec: cmapi.CertificateRequestSpec{
IssuerRef: cmmeta.ObjectReference{
Name: "testissuer",
Kind: "IssuerKind",
Group: "group.example.com",
},
Request: testcrypto.MustGenerateCSRImpl(t, testcrypto.MustCreatePEMPrivateKey(t), &cmapi.Certificate{Spec: cmapi.CertificateSpec{
CommonName: "example.com",
}}),
}},
reason: SecretMismatch,
message: "Secret contains a private key that does not match the current CertificateRequest",
reissue: true,
},
// we only have a basic test here for this as unit tests for the
// `pki.RequestMatchesSpec` function cover all other cases.
"trigger issuance when CertificateRequest does not match certificate spec": {
Expand Down
4 changes: 4 additions & 0 deletions internal/controller/certificates/policies/constants.go
Expand Up @@ -29,6 +29,10 @@ const (
// InvalidCertificate is a policy violation whereby the signed certificate in
// the Input Secret could not be parsed or decoded.
InvalidCertificate string = "InvalidCertificate"
// InvalidCertificateRequest is a policy violation whereby the CSR in
// the Input CertificateRequest could not be parsed or decoded.
InvalidCertificateRequest string = "InvalidCertificateRequest"

// SecretMismatch is a policy violation reason for a scenario where Secret's
// private key does not match spec.
SecretMismatch string = "SecretMismatch"
Expand Down
2 changes: 2 additions & 0 deletions internal/controller/certificates/policies/policies.go
Expand Up @@ -72,6 +72,7 @@ func NewTriggerPolicyChain(c clock.Clock) Chain {
SecretPublicKeysDiffer,
SecretPrivateKeyMatchesSpec,
SecretIssuerAnnotationsNotUpToDate,
SecretPublicKeyDiffersFromCurrentCertificateRequest,
CurrentCertificateRequestNotValidForSpec,
CurrentCertificateNearingExpiry(c),
}
Expand All @@ -84,6 +85,7 @@ func NewReadinessPolicyChain(c clock.Clock) Chain {
SecretDoesNotExist,
SecretIsMissingData,
SecretPublicKeysDiffer,
SecretPublicKeyDiffersFromCurrentCertificateRequest,
CurrentCertificateRequestNotValidForSpec,
CurrentCertificateHasExpired(c),
}
Expand Down
Expand Up @@ -467,7 +467,11 @@ func TestNewReadinessPolicyChain(t *testing.T) {
Name: "testissuer",
Kind: "IssuerKind",
Group: "group.example.com",
})),
}),
gen.SetCertificateRequestCSR(testcrypto.MustGenerateCSRImpl(t, privKey,
gen.Certificate("something",
gen.SetCertificateCommonName("new.example.com")))),
),
reason: policies.Expired,
message: "Certificate expired on Sun, 31 Dec 0000 23:00:00 UTC",
violationFound: true,
Expand Down