|
| 1 | +package certs_test |
| 2 | + |
| 3 | +import ( |
| 4 | + "crypto/x509" |
| 5 | + "encoding/pem" |
| 6 | + "math/big" |
| 7 | + "net" |
| 8 | + "sort" |
| 9 | + "time" |
| 10 | + |
| 11 | + . "github.com/onsi/ginkgo" |
| 12 | + . "github.com/onsi/gomega" |
| 13 | + . "github.com/onsi/gomega/gstruct" |
| 14 | + |
| 15 | + "sigs.k8s.io/controller-runtime/pkg/internal/testing/certs" |
| 16 | +) |
| 17 | + |
| 18 | +var _ = Describe("TinyCA", func() { |
| 19 | + var ca *certs.TinyCA |
| 20 | + |
| 21 | + BeforeEach(func() { |
| 22 | + var err error |
| 23 | + ca, err = certs.NewTinyCA() |
| 24 | + Expect(err).NotTo(HaveOccurred(), "should be able to initialize the CA") |
| 25 | + }) |
| 26 | + |
| 27 | + Describe("the CA certs themselves", func() { |
| 28 | + It("should be retrievable as a cert pair", func() { |
| 29 | + Expect(ca.CA.Key).NotTo(BeNil(), "should have a key") |
| 30 | + Expect(ca.CA.Cert).NotTo(BeNil(), "should have a cert") |
| 31 | + }) |
| 32 | + |
| 33 | + It("should be usable for signing & verifying", func() { |
| 34 | + Expect(ca.CA.Cert.KeyUsage&x509.KeyUsageCertSign).NotTo(Equal(0), "should be usable for cert signing") |
| 35 | + Expect(ca.CA.Cert.KeyUsage&x509.KeyUsageDigitalSignature).NotTo(Equal(0), "should be usable for signature verifying") |
| 36 | + }) |
| 37 | + }) |
| 38 | + |
| 39 | + It("should produce unique serials among all generated certificates of all types", func() { |
| 40 | + By("generating a few cert pairs for both serving and client auth") |
| 41 | + firstCerts, err := ca.NewServingCert() |
| 42 | + Expect(err).NotTo(HaveOccurred()) |
| 43 | + secondCerts, err := ca.NewClientCert(certs.ClientInfo{Name: "user"}) |
| 44 | + Expect(err).NotTo(HaveOccurred()) |
| 45 | + thirdCerts, err := ca.NewServingCert() |
| 46 | + Expect(err).NotTo(HaveOccurred()) |
| 47 | + |
| 48 | + By("checking that they have different serials") |
| 49 | + serials := []*big.Int{ |
| 50 | + firstCerts.Cert.SerialNumber, |
| 51 | + secondCerts.Cert.SerialNumber, |
| 52 | + thirdCerts.Cert.SerialNumber, |
| 53 | + } |
| 54 | + // quick uniqueness check of numbers: sort, then you only have to compare sequential entries |
| 55 | + sort.Slice(serials, func(i, j int) bool { |
| 56 | + return serials[i].Cmp(serials[j]) == -1 |
| 57 | + }) |
| 58 | + Expect(serials[1].Cmp(serials[0])).NotTo(Equal(0), "serials shouldn't be equal") |
| 59 | + Expect(serials[2].Cmp(serials[1])).NotTo(Equal(0), "serials shouldn't be equal") |
| 60 | + }) |
| 61 | + |
| 62 | + Describe("Generated serving certs", func() { |
| 63 | + It("should be valid for short enough to avoid production usage, but long enough for long-running tests", func() { |
| 64 | + cert, err := ca.NewServingCert() |
| 65 | + Expect(err).NotTo(HaveOccurred(), "should be able to generate the serving certs") |
| 66 | + |
| 67 | + duration := cert.Cert.NotAfter.Sub(time.Now()) |
| 68 | + Expect(duration).To(BeNumerically("<=", 168*time.Hour), "not-after should be short-ish (<= 1 week)") |
| 69 | + Expect(duration).To(BeNumerically(">=", 2*time.Hour), "not-after should be enough for long tests (couple of hours)") |
| 70 | + }) |
| 71 | + |
| 72 | + Context("when encoding names", func() { |
| 73 | + var cert certs.CertPair |
| 74 | + BeforeEach(func() { |
| 75 | + By("generating a serving cert with IPv4 & IPv6 addresses, and DNS names") |
| 76 | + var err error |
| 77 | + // IPs are in the "example & docs" blocks for IPv4 (TEST-NET-1) & IPv6 |
| 78 | + cert, err = ca.NewServingCert("192.0.2.1", "localhost", "2001:db8::") |
| 79 | + Expect(err).NotTo(HaveOccurred(), "should be able to create the serving certs") |
| 80 | + }) |
| 81 | + |
| 82 | + It("should encode all non-IP names as DNS SANs", func() { |
| 83 | + Expect(cert.Cert.DNSNames).To(ConsistOf("localhost")) |
| 84 | + }) |
| 85 | + |
| 86 | + It("should encode all IP names as IP SANs", func() { |
| 87 | + // NB(directxman12): this is non-exhaustive because we also |
| 88 | + // convert DNS SANs to IPs too (see test below) |
| 89 | + Expect(cert.Cert.IPAddresses).To(ContainElements( |
| 90 | + // normalize the elements with To16 so we can compare them to the output of |
| 91 | + // of ParseIP safely (the alternative is a custom matcher that calls Equal, |
| 92 | + // but this is easier) |
| 93 | + WithTransform(net.IP.To16, Equal(net.ParseIP("192.0.2.1"))), |
| 94 | + WithTransform(net.IP.To16, Equal(net.ParseIP("2001:db8::"))), |
| 95 | + )) |
| 96 | + }) |
| 97 | + |
| 98 | + It("should add the corresponding IP address(es) (as IP SANs) for DNS names", func() { |
| 99 | + // NB(directxman12): we currently fail if the lookup fails. |
| 100 | + // I'm not certain this is the best idea (both the bailing on |
| 101 | + // error and the actual idea), so if this causes issues, you |
| 102 | + // might want to reconsider. |
| 103 | + |
| 104 | + localhostAddrs, err := net.LookupHost("localhost") |
| 105 | + Expect(err).NotTo(HaveOccurred(), "should be able to find IPs for localhost") |
| 106 | + localhostIPs := make([]interface{}, len(localhostAddrs)) |
| 107 | + for i, addr := range localhostAddrs { |
| 108 | + // normalize the elements with To16 so we can compare them to the output of |
| 109 | + // of ParseIP safely (the alternative is a custom matcher that calls Equal, |
| 110 | + // but this is easier) |
| 111 | + localhostIPs[i] = WithTransform(net.IP.To16, Equal(net.ParseIP(addr))) |
| 112 | + } |
| 113 | + Expect(cert.Cert.IPAddresses).To(ContainElements(localhostIPs...)) |
| 114 | + }) |
| 115 | + }) |
| 116 | + |
| 117 | + It("should assume a name of localhost (DNS SAN) if no names are given", func() { |
| 118 | + cert, err := ca.NewServingCert() |
| 119 | + Expect(err).NotTo(HaveOccurred(), "should be able to generate a serving cert with the default name") |
| 120 | + Expect(cert.Cert.DNSNames).To(ConsistOf("localhost"), "the default DNS name should be localhost") |
| 121 | + |
| 122 | + }) |
| 123 | + |
| 124 | + It("should be usable for server auth, verifying, and enciphering", func() { |
| 125 | + cert, err := ca.NewServingCert() |
| 126 | + Expect(err).NotTo(HaveOccurred(), "should be able to generate a serving cert") |
| 127 | + |
| 128 | + Expect(cert.Cert.KeyUsage&x509.KeyUsageKeyEncipherment).NotTo(Equal(0), "should be usable for key enciphering") |
| 129 | + Expect(cert.Cert.KeyUsage&x509.KeyUsageDigitalSignature).NotTo(Equal(0), "should be usable for signature verifying") |
| 130 | + Expect(cert.Cert.ExtKeyUsage).To(ContainElement(x509.ExtKeyUsageServerAuth), "should be usable for server auth") |
| 131 | + |
| 132 | + }) |
| 133 | + |
| 134 | + It("should be signed by the CA", func() { |
| 135 | + cert, err := ca.NewServingCert() |
| 136 | + Expect(err).NotTo(HaveOccurred(), "should be able to generate a serving cert") |
| 137 | + Expect(cert.Cert.CheckSignatureFrom(ca.CA.Cert)).To(Succeed()) |
| 138 | + }) |
| 139 | + }) |
| 140 | + |
| 141 | + Describe("Generated client certs", func() { |
| 142 | + var cert certs.CertPair |
| 143 | + BeforeEach(func() { |
| 144 | + var err error |
| 145 | + cert, err = ca.NewClientCert(certs.ClientInfo{ |
| 146 | + Name: "user", |
| 147 | + Groups: []string{"group1", "group2"}, |
| 148 | + }) |
| 149 | + Expect(err).NotTo(HaveOccurred(), "should be able to create a client cert") |
| 150 | + }) |
| 151 | + |
| 152 | + It("should be valid for short enough to avoid production usage, but long enough for long-running tests", func() { |
| 153 | + duration := cert.Cert.NotAfter.Sub(time.Now()) |
| 154 | + Expect(duration).To(BeNumerically("<=", 168*time.Hour), "not-after should be short-ish (<= 1 week)") |
| 155 | + Expect(duration).To(BeNumerically(">=", 2*time.Hour), "not-after should be enough for long tests (couple of hours)") |
| 156 | + }) |
| 157 | + |
| 158 | + It("should be usable for client auth, verifying, and enciphering", func() { |
| 159 | + Expect(cert.Cert.KeyUsage&x509.KeyUsageKeyEncipherment).NotTo(Equal(0), "should be usable for key enciphering") |
| 160 | + Expect(cert.Cert.KeyUsage&x509.KeyUsageDigitalSignature).NotTo(Equal(0), "should be usable for signature verifying") |
| 161 | + Expect(cert.Cert.ExtKeyUsage).To(ContainElement(x509.ExtKeyUsageClientAuth), "should be usable for client auth") |
| 162 | + }) |
| 163 | + |
| 164 | + It("should encode the user name as the common name", func() { |
| 165 | + Expect(cert.Cert.Subject.CommonName).To(Equal("user")) |
| 166 | + }) |
| 167 | + |
| 168 | + It("should encode the groups as the organization values", func() { |
| 169 | + Expect(cert.Cert.Subject.Organization).To(ConsistOf("group1", "group2")) |
| 170 | + }) |
| 171 | + |
| 172 | + It("should be signed by the CA", func() { |
| 173 | + Expect(cert.Cert.CheckSignatureFrom(ca.CA.Cert)).To(Succeed()) |
| 174 | + }) |
| 175 | + }) |
| 176 | +}) |
| 177 | + |
| 178 | +var _ = Describe("Certificate Pairs", func() { |
| 179 | + var pair certs.CertPair |
| 180 | + BeforeEach(func() { |
| 181 | + ca, err := certs.NewTinyCA() |
| 182 | + Expect(err).NotTo(HaveOccurred(), "should be able to generate a cert pair") |
| 183 | + |
| 184 | + pair = ca.CA |
| 185 | + }) |
| 186 | + |
| 187 | + Context("when serializing just the public key", func() { |
| 188 | + It("should serialize into a CERTIFICATE PEM block", func() { |
| 189 | + bytes := pair.CertBytes() |
| 190 | + Expect(bytes).NotTo(BeEmpty(), "should produce some cert bytes") |
| 191 | + |
| 192 | + block, rest := pem.Decode(bytes) |
| 193 | + Expect(rest).To(BeEmpty(), "shouldn't have any data besides the PEM block") |
| 194 | + |
| 195 | + Expect(block).To(PointTo(MatchAllFields(Fields{ |
| 196 | + "Type": Equal("CERTIFICATE"), |
| 197 | + "Headers": BeEmpty(), |
| 198 | + "Bytes": Equal(pair.Cert.Raw), |
| 199 | + }))) |
| 200 | + }) |
| 201 | + }) |
| 202 | + |
| 203 | + Context("when serializing both parts", func() { |
| 204 | + var certBytes, keyBytes []byte |
| 205 | + BeforeEach(func() { |
| 206 | + var err error |
| 207 | + certBytes, keyBytes, err = pair.AsBytes() |
| 208 | + Expect(err).NotTo(HaveOccurred(), "should be able to serialize the pair") |
| 209 | + }) |
| 210 | + |
| 211 | + It("should serialize the private key in PKCS8 form in a PRIVATE KEY PEM block", func() { |
| 212 | + Expect(keyBytes).NotTo(BeEmpty(), "should produce some key bytes") |
| 213 | + |
| 214 | + By("decoding & checking the PEM block") |
| 215 | + block, rest := pem.Decode(keyBytes) |
| 216 | + Expect(rest).To(BeEmpty(), "shouldn't have any data besides the PEM block") |
| 217 | + |
| 218 | + Expect(block.Type).To(Equal("PRIVATE KEY")) |
| 219 | + |
| 220 | + By("decoding & checking the PKCS8 data") |
| 221 | + Expect(x509.ParsePKCS8PrivateKey(block.Bytes)).NotTo(BeNil(), "should be able to parse back the private key") |
| 222 | + }) |
| 223 | + |
| 224 | + It("should serialize the public key into a CERTIFICATE PEM block", func() { |
| 225 | + Expect(certBytes).NotTo(BeEmpty(), "should produce some cert bytes") |
| 226 | + |
| 227 | + block, rest := pem.Decode(certBytes) |
| 228 | + Expect(rest).To(BeEmpty(), "shouldn't have any data besides the PEM block") |
| 229 | + |
| 230 | + Expect(block).To(PointTo(MatchAllFields(Fields{ |
| 231 | + "Type": Equal("CERTIFICATE"), |
| 232 | + "Headers": BeEmpty(), |
| 233 | + "Bytes": Equal(pair.Cert.Raw), |
| 234 | + }))) |
| 235 | + }) |
| 236 | + |
| 237 | + }) |
| 238 | +}) |
0 commit comments