Use Kubernetes CSR spec.expirationSeconds to express cert duration

This change adds the ability to express certificate duration using
the Kubernetes CSR spec.expirationSeconds field alongside the existing
approach of using the experimental.cert-manager.io/request-duration
annotation.  Both approaches are supported as the expirationSeconds
field requires Kubernetes v1.22+.

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2022-03-17 14:22:53 -04:00
parent 068c5f0870
commit 2a33c7a5c2
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
9 changed files with 187 additions and 11 deletions

View File

@ -372,7 +372,10 @@ func buildCertificateSigningRequest(crt *cmapi.Certificate, pk []byte, crName, s
}
csr.Annotations[experimentalapi.CertificateSigningRequestIsCAAnnotationKey] = strconv.FormatBool(crt.Spec.IsCA)
if crt.Spec.Duration != nil {
csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey] = crt.Spec.Duration.Duration.String()
duration := crt.Spec.Duration.Duration
csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey] = duration.String()
seconds := int32(duration.Seconds()) // technically this could overflow but I do not think it matters
csr.Spec.ExpirationSeconds = &seconds // if this is less than 600, the API server will fail the request
}
return csr, nil

View File

@ -649,6 +649,41 @@ func TestCA_Sign(t *testing.T) {
assert.LessOrEqualf(t, deltaSec, 2., "expected a time delta lower than 2 second. Time expected='%s', got='%s'", expectNotAfter.String(), got.NotAfter.String())
},
},
"when the CertificateSigningRequest has the expiration seconds field set, it should appear as notAfter on the signed ca": {
givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{
SecretName: "secret-1",
})),
givenCSR: gen.CertificateSigningRequest("csr-1",
gen.SetCertificateSigningRequestRequest(testCSR),
gen.SetCertificateSigningRequestSignerName("issers.cert-manager.io/"+gen.DefaultTestNamespace+".issuer-1"),
gen.SetCertificateSigningRequestExpirationSeconds(654),
),
assertSignedCert: func(t *testing.T, got *x509.Certificate) {
// Although there is less than 1µs between the time.Now
// call made by the certificate template func (in the "pki"
// package) and the time.Now below, rounding or truncating
// will always end up with a flaky test. This is due to the
// rounding made to the notAfter value when serializing the
// certificate to ASN.1 [1].
//
// [1]: https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
//
// So instead of using a truncation or rounding in order to
// check the time, we use a delta of 2 seconds. One entire
// second is totally overkill since, as detailed above, the
// delay is probably less than a microsecond. But that will
// do for now!
//
// Note that we do have a plan to fix this. We want to be
// injecting a time (instead of time.Now) to the template
// functions. This work is being tracked in this issue:
// https://github.com/cert-manager/cert-manager/issues/3738
expectNotAfter := time.Now().UTC().Add(654 * time.Second)
deltaSec := math.Abs(expectNotAfter.Sub(got.NotAfter).Seconds())
assert.LessOrEqualf(t, deltaSec, 2., "expected a time delta lower than 2 second. Time expected='%s', got='%s'", expectNotAfter.String(), got.NotAfter.String())
},
},
"when the CertificateSigningRequest has the isCA field set, it should appear on the signed ca": {
givenCASecret: gen.SecretFrom(gen.Secret("secret-1"), gen.SetSecretNamespace("default"), gen.SetSecretData(secretDataFor(t, rootPK, rootCert))),
givenCAIssuer: gen.Issuer("issuer-1", gen.SetIssuerCA(cmapi.CAIssuer{

View File

@ -732,6 +732,41 @@ func TestSign(t *testing.T) {
assert.LessOrEqualf(t, deltaSec, 2., "expected a time delta lower than 2 second. Time expected='%s', got='%s'", expectNotAfter.String(), got.NotAfter.String())
},
},
"when the CertificateSigningRequest has the expiration seconds field set, it should appear as notAfter on the signed certificate": {
csr: gen.CertificateSigningRequest("csr-1",
gen.AddCertificateSigningRequestAnnotations(map[string]string{
"experimental.cert-manager.io/private-key-secret-name": "test-secret",
}),
gen.SetCertificateSigningRequestSignerName("issuers.cert-manager.io/default-unit-test-ns.issuer-1"),
gen.SetCertificateSigningRequestExpirationSeconds(444),
gen.SetCertificateSigningRequestRequest(csrBundle.csrPEM),
),
issuer: baseIssuer,
assertSignedCert: func(t *testing.T, got *x509.Certificate) {
// Although there is less than 1µs between the time.Now
// call made by the certificate template func (in the "pki"
// package) and the time.Now below, rounding or truncating
// will always end up with a flaky test. This is due to the
// rounding made to the notAfter value when serializing the
// certificate to ASN.1 [1].
//
// [1]: https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
//
// So instead of using a truncation or rounding in order to
// check the time, we use a delta of 2 seconds. One entire
// second is totally overkill since, as detailed above, the
// delay is probably less than a microsecond. But that will
// do for now!
//
// Note that we do have a plan to fix this. We want to be
// injecting a time (instead of time.Now) to the template
// functions. This work is being tracked in this issue:
// https://github.com/cert-manager/cert-manager/issues/3738
expectNotAfter := time.Now().UTC().Add(444 * time.Second)
deltaSec := math.Abs(expectNotAfter.Sub(got.NotAfter).Seconds())
assert.LessOrEqualf(t, deltaSec, 2., "expected a time delta lower than 2 second. Time expected='%s', got='%s'", expectNotAfter.String(), got.NotAfter.String())
},
},
"when the CertificateSigningRequest has the isCA field set, it should appear on the signed certificate": {
csr: gen.CertificateSigningRequest("csr-1",
gen.AddCertificateSigningRequestAnnotations(map[string]string{

View File

@ -48,12 +48,18 @@ func GenerateTemplateFromCertificateSigningRequest(csr *certificatesv1.Certifica
// DurationFromCertificateSigningRequest returns the duration that the user may
// have requested using the annotation
// "experimental.cert-manager.io/request-duration".
// "experimental.cert-manager.io/request-duration" or via the CSR
// spec.expirationSeconds field (the annotation is preferred since it predates
// the field which is only available in Kubernetes v1.22+).
// Returns the cert-manager default certificate duration when the user hasn't
// provided the annotation.
// provided the annotation or spec.expirationSeconds.
func DurationFromCertificateSigningRequest(csr *certificatesv1.CertificateSigningRequest) (time.Duration, error) {
requestedDuration, ok := csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey]
if !ok {
if csr.Spec.ExpirationSeconds != nil {
return time.Duration(*csr.Spec.ExpirationSeconds) * time.Second, nil
}
// The user may not have set a duration annotation. Use the default
// duration in this case.
return cmapi.DefaultCertificateDuration, nil

View File

@ -131,6 +131,73 @@ func TestGenerateTemplateFromCertificateSigningRequest(t *testing.T) {
DNSNames: []string{"example.com", "foo.example.com"},
},
},
"a CSR with expiration seconds that is valid should return a valid *x509.Certificate": {
csr: gen.CertificateSigningRequest("",
gen.SetCertificateSigningRequestExpirationSeconds(999),
gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
certificatesv1.UsageAny,
certificatesv1.UsageDigitalSignature,
certificatesv1.UsageCRLSign,
certificatesv1.UsageCodeSigning,
certificatesv1.UsageContentCommitment,
}),
gen.SetCertificateSigningRequestIsCA(false),
gen.SetCertificateSigningRequestRequest(csr),
),
expCertificate: &x509.Certificate{
Version: 2,
BasicConstraintsValid: true,
SerialNumber: nil,
PublicKeyAlgorithm: x509.RSA,
PublicKey: pk.Public(),
IsCA: false,
Subject: pkix.Name{
CommonName: "example.com",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(999 * time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageContentCommitment,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageAny,
x509.ExtKeyUsageCodeSigning,
},
DNSNames: []string{"example.com", "foo.example.com"},
},
},
"a CSR with expiration seconds and duration annotation should prefer the annotation duration": {
csr: gen.CertificateSigningRequest("",
gen.SetCertificateSigningRequestExpirationSeconds(999),
gen.SetCertificateSigningRequestDuration("777s"),
gen.SetCertificateSigningRequestUsages([]certificatesv1.KeyUsage{
certificatesv1.UsageAny,
certificatesv1.UsageDigitalSignature,
certificatesv1.UsageCRLSign,
certificatesv1.UsageCodeSigning,
certificatesv1.UsageContentCommitment,
}),
gen.SetCertificateSigningRequestIsCA(false),
gen.SetCertificateSigningRequestRequest(csr),
),
expCertificate: &x509.Certificate{
Version: 2,
BasicConstraintsValid: true,
SerialNumber: nil,
PublicKeyAlgorithm: x509.RSA,
PublicKey: pk.Public(),
IsCA: false,
Subject: pkix.Name{
CommonName: "example.com",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(777 * time.Second),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCRLSign | x509.KeyUsageContentCommitment,
ExtKeyUsage: []x509.ExtKeyUsage{
x509.ExtKeyUsageAny,
x509.ExtKeyUsageCodeSigning,
},
DNSNames: []string{"example.com", "foo.example.com"},
},
},
}
for name, test := range tests {

View File

@ -194,8 +194,12 @@ func ExpectValidDuration(csr *certificatesv1.CertificateSigningRequest, _ crypto
var expectedDuration time.Duration
durationString, ok := csr.Annotations[experimentalapi.CertificateSigningRequestDurationAnnotationKey]
if !ok {
// If duration wasn't requested, then we match against the default.
expectedDuration = cmapi.DefaultCertificateDuration
if csr.Spec.ExpirationSeconds != nil {
expectedDuration = time.Duration(*csr.Spec.ExpirationSeconds) * time.Second
} else {
// If duration wasn't requested, then we match against the default.
expectedDuration = cmapi.DefaultCertificateDuration
}
} else {
expectedDuration, err = time.ParseDuration(durationString)
if err != nil {

View File

@ -24,6 +24,7 @@ go_library(
"@io_k8s_api//certificates/v1:go_default_library",
"@io_k8s_api//core/v1:go_default_library",
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
"@io_k8s_utils//pointer:go_default_library",
],
)

View File

@ -28,6 +28,7 @@ import (
certificatesv1 "k8s.io/api/certificates/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
experimentalapi "github.com/cert-manager/cert-manager/pkg/apis/experimental/v1alpha1"
"github.com/cert-manager/cert-manager/pkg/util"
@ -76,9 +77,10 @@ func (s *Suite) Define() {
// csrModifers define the shape of the X.509 CSR which is used in the
// test case. We use a function to allow access to variables that are
// initialized at test runtime by complete().
csrModifiers func() []gen.CSRModifier
kubeCSRUsages []certificatesv1.KeyUsage
kubeCSRAnnotations map[string]string
csrModifiers func() []gen.CSRModifier
kubeCSRUsages []certificatesv1.KeyUsage
kubeCSRAnnotations map[string]string
kubeCSRExpirationSeconds *int32
// The list of features that are required by the Issuer for the test to
// run.
requiredFeatures []featureset.Feature
@ -252,6 +254,22 @@ func (s *Suite) Define() {
requiredFeatures: []featureset.Feature{featureset.DurationFeature},
},
"should issue a certificate that defines a Common Name, DNS Name, and sets a duration via expiration seconds": {
keyAlgo: x509.RSA,
csrModifiers: func() []gen.CSRModifier {
return []gen.CSRModifier{
gen.SetCSRDNSNames(sharedCommonName),
gen.SetCSRDNSNames(sharedCommonName),
}
},
kubeCSRUsages: []certificatesv1.KeyUsage{
certificatesv1.UsageDigitalSignature,
certificatesv1.UsageKeyEncipherment,
},
kubeCSRExpirationSeconds: pointer.Int32(3333),
requiredFeatures: []featureset.Feature{featureset.DurationFeature},
},
"should issue a certificate that defines a DNS Name and sets a duration": {
keyAlgo: x509.RSA,
csrModifiers: func() []gen.CSRModifier {
@ -353,9 +371,10 @@ func (s *Suite) Define() {
Annotations: test.kubeCSRAnnotations,
},
Spec: certificatesv1.CertificateSigningRequestSpec{
Request: csr,
SignerName: signerName,
Usages: test.kubeCSRUsages,
Request: csr,
SignerName: signerName,
Usages: test.kubeCSRUsages,
ExpirationSeconds: test.kubeCSRExpirationSeconds,
},
}

View File

@ -95,6 +95,12 @@ func SetCertificateSigningRequestSignerName(signerName string) CertificateSignin
}
}
func SetCertificateSigningRequestExpirationSeconds(seconds int32) CertificateSigningRequestModifier {
return func(csr *certificatesv1.CertificateSigningRequest) {
csr.Spec.ExpirationSeconds = &seconds
}
}
func SetCertificateSigningRequestDuration(duration string) CertificateSigningRequestModifier {
return AddCertificateSigningRequestAnnotations(map[string]string{
experimentalapi.CertificateSigningRequestDurationAnnotationKey: duration,