/* Copyright 2019 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package authority import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "math/big" "net/url" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" capi "k8s.io/api/certificates/v1" "k8s.io/apimachinery/pkg/util/diff" ) func TestCertificateAuthority(t *testing.T) { caKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) if err != nil { t.Fatal(err) } now := time.Now() nowFunc := func() time.Time { return now } tmpl := &x509.Certificate{ SerialNumber: big.NewInt(42), Subject: pkix.Name{ CommonName: "test-ca", }, NotBefore: now.Add(-24 * time.Hour), NotAfter: now.Add(24 * time.Hour), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, } der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, caKey.Public(), caKey) if err != nil { t.Fatal(err) } caCert, err := x509.ParseCertificate(der) if err != nil { t.Fatal(err) } uri, err := url.Parse("help://me@what:8080/where/when?why=true") if err != nil { t.Fatal(err) } tests := []struct { name string cr x509.CertificateRequest policy SigningPolicy mutateCA func(ca *CertificateAuthority) want x509.Certificate wantErr string }{ { name: "ca info", policy: PermissiveSigningPolicy{TTL: time.Hour, Now: nowFunc}, want: x509.Certificate{ Issuer: caCert.Subject, AuthorityKeyId: caCert.SubjectKeyId, NotBefore: now, NotAfter: now.Add(1 * time.Hour), BasicConstraintsValid: true, }, }, { name: "key usage", policy: PermissiveSigningPolicy{TTL: time.Hour, Usages: []capi.KeyUsage{"signing"}, Now: nowFunc}, want: x509.Certificate{ NotBefore: now, NotAfter: now.Add(1 * time.Hour), BasicConstraintsValid: true, KeyUsage: x509.KeyUsageDigitalSignature, }, }, { name: "ext key usage", policy: PermissiveSigningPolicy{TTL: time.Hour, Usages: []capi.KeyUsage{"client auth"}, Now: nowFunc}, want: x509.Certificate{ NotBefore: now, NotAfter: now.Add(1 * time.Hour), BasicConstraintsValid: true, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }, }, { name: "backdate without short", policy: PermissiveSigningPolicy{TTL: time.Hour, Backdate: 5 * time.Minute, Now: nowFunc}, want: x509.Certificate{ NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(55 * time.Minute), BasicConstraintsValid: true, }, }, { name: "backdate without short and super small ttl", policy: PermissiveSigningPolicy{TTL: time.Minute, Backdate: 5 * time.Minute, Now: nowFunc}, want: x509.Certificate{ NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(-4 * time.Minute), BasicConstraintsValid: true, }, }, { name: "backdate with short", policy: PermissiveSigningPolicy{TTL: time.Hour, Backdate: 5 * time.Minute, Short: 8 * time.Hour, Now: nowFunc}, want: x509.Certificate{ NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(time.Hour), BasicConstraintsValid: true, }, }, { name: "backdate with short and super small ttl", policy: PermissiveSigningPolicy{TTL: time.Minute, Backdate: 5 * time.Minute, Short: 8 * time.Hour, Now: nowFunc}, want: x509.Certificate{ NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(time.Minute), BasicConstraintsValid: true, }, }, { name: "backdate with short but longer ttl", policy: PermissiveSigningPolicy{TTL: 24 * time.Hour, Backdate: 5 * time.Minute, Short: 8 * time.Hour, Now: nowFunc}, want: x509.Certificate{ NotBefore: now.Add(-5 * time.Minute), NotAfter: now.Add(24*time.Hour - 5*time.Minute), BasicConstraintsValid: true, }, }, { name: "truncate expiration", policy: PermissiveSigningPolicy{TTL: 48 * time.Hour, Now: nowFunc}, want: x509.Certificate{ NotBefore: now, NotAfter: now.Add(24 * time.Hour), BasicConstraintsValid: true, }, }, { name: "uri sans", policy: PermissiveSigningPolicy{TTL: time.Hour, Now: nowFunc}, cr: x509.CertificateRequest{ URIs: []*url.URL{uri}, }, want: x509.Certificate{ URIs: []*url.URL{uri}, NotBefore: now, NotAfter: now.Add(1 * time.Hour), BasicConstraintsValid: true, }, }, { name: "expired ca", policy: PermissiveSigningPolicy{TTL: time.Hour, Now: nowFunc}, mutateCA: func(ca *CertificateAuthority) { ca.Certificate.NotAfter = now // pretend that the CA has expired }, wantErr: "the signer has expired: NotAfter=" + now.String(), }, { name: "expired ca with backdate", policy: PermissiveSigningPolicy{TTL: time.Hour, Backdate: 5 * time.Minute, Now: nowFunc}, mutateCA: func(ca *CertificateAuthority) { ca.Certificate.NotAfter = now // pretend that the CA has expired }, wantErr: "refusing to sign a certificate that expired in the past: NotAfter=" + now.String(), }, } crKey, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader) if err != nil { t.Fatal(err) } for _, test := range tests { t.Run(test.name, func(t *testing.T) { caCertShallowCopy := *caCert ca := &CertificateAuthority{ Certificate: &caCertShallowCopy, PrivateKey: caKey, } if test.mutateCA != nil { test.mutateCA(ca) } csr, err := x509.CreateCertificateRequest(rand.Reader, &test.cr, crKey) if err != nil { t.Fatal(err) } certDER, err := ca.Sign(csr, test.policy) if len(test.wantErr) > 0 { if errStr := errString(err); test.wantErr != errStr { t.Fatalf("expected error %s but got %s", test.wantErr, errStr) } return } if err != nil { t.Fatal(err) } cert, err := x509.ParseCertificate(certDER) if err != nil { t.Fatal(err) } opts := cmp.Options{ cmpopts.IgnoreFields(x509.Certificate{}, "SignatureAlgorithm", "PublicKeyAlgorithm", "Version", "MaxPathLen", ), diff.IgnoreUnset(), cmp.Transformer("RoundTime", func(x time.Time) time.Time { return x.Truncate(time.Second) }), cmp.Comparer(func(x, y *url.URL) bool { return ((x == nil) && (y == nil)) || x.String() == y.String() }), } if !cmp.Equal(*cert, test.want, opts) { t.Errorf("unexpected diff: %v", cmp.Diff(*cert, test.want, opts)) } }) } } func errString(err error) string { if err == nil { return "" } return err.Error() }