diff --git a/pkg/util/pki/parse.go b/pkg/util/pki/parse.go index 06460443b..1d562d748 100644 --- a/pkg/util/pki/parse.go +++ b/pkg/util/pki/parse.go @@ -140,230 +140,3 @@ func DecodeX509CertificateRequestBytes(csrBytes []byte) (*x509.CertificateReques return csr, nil } - -// PEMBundle includes the PEM encoded X.509 certificate chain and CA. CAPEM -// contains either 1 CA certificate, or is empty if only a single certificate -// exists in the chain. -type PEMBundle struct { - CAPEM []byte - ChainPEM []byte -} - -type chainNode struct { - cert *x509.Certificate - issuer *chainNode -} - -// ParseSingleCertificateChainPEM decodes a PEM encoded certificate chain before -// calling ParseSingleCertificateChainPEM -func ParseSingleCertificateChainPEM(pembundle []byte) (PEMBundle, error) { - certs, err := DecodeX509CertificateChainBytes(pembundle) - if err != nil { - return PEMBundle{}, err - } - return ParseSingleCertificateChain(certs) -} - -// ParseSingleCertificateChain returns the PEM-encoded chain of certificates as -// well as the PEM-encoded CA certificate. -// -// The CA (CAPEM) may not be a true root, but the highest intermediate certificate. -// The certificate is chosen as follows: -// - If the chain has a self-signed root, the root certificate. -// - If the chain has no self-signed root and has > 1 certificates, the highest certificate in the chain. -// - If the chain has no self-signed root and has == 1 certificate, nil. -// -// The certificate chain (ChainPEM) starts with the leaf certificate and ends with the -// highest certificate in the chain which is not self-signed. Self-signed certificates -// are not included in the chain because we are certain they are known and trusted by the -// client already. -// -// This function removes duplicate certificate entries as well as comments and -// unnecessary white space. -// -// An error is returned if the passed bundle is not a valid single chain, -// the bundle is malformed, or the chain is broken. -func ParseSingleCertificateChain(certs []*x509.Certificate) (PEMBundle, error) { - // De-duplicate certificates. This moves "complicated" logic away from - // consumers and into a shared function, who would otherwise have to do this - // anyway. - for i := 0; i < len(certs)-1; i++ { - for j := 1; j < len(certs); j++ { - if i == j { - continue - } - if certs[i].Equal(certs[j]) { - certs = append(certs[:j], certs[j+1:]...) - } - } - } - - // A certificate chain can be well described as a linked list. Here we build - // multiple lists that contain a single node, each being a single certificate - // that was passed. - var chains []*chainNode - for i := range certs { - chains = append(chains, &chainNode{cert: certs[i]}) - } - - // The task is to build a single list which represents a single certificate - // chain. The strategy is to iteratively attempt to join items in the list to - // build this single chain. Once we have a single list, we have built the - // chain. If the number of lists do not decrease after a pass, then the list - // can never be reduced to a single chain and we error. - for { - // If a single list is left, then we have built the entire chain. Stop - // iterating. - if len(chains) == 1 { - break - } - - // lastChainsLength is used to ensure that at every pass, the number of - // tested chains gets smaller. - lastChainsLength := len(chains) - for i := 0; i < len(chains)-1; i++ { - for j := 1; j < len(chains); j++ { - if i == j { - continue - } - - // attempt to add both chains together - chain, ok := chains[i].tryMergeChain(chains[j]) - if ok { - // If adding the chains together was successful, remove inner chain from - // list - chains = append(chains[:j], chains[j+1:]...) - } - - chains[i] = chain - } - } - - // If no chains were merged in this pass, the chain can never be built as a - // single list. Error. - if lastChainsLength == len(chains) { - return PEMBundle{}, errors.NewInvalidData("certificate chain is malformed or broken") - } - } - - // There is only a single chain left at index 0. Return chain as PEM. - return chains[0].toBundleAndCA() -} - -// toBundleAndCA will return the PEM bundle of this chain. -func (c *chainNode) toBundleAndCA() (PEMBundle, error) { - var ( - certs []*x509.Certificate - ca *x509.Certificate - ) - - for { - // If the issuer is nil, we have hit the root of the chain. Assign the CA - // to this certificate and stop traversing. If the certificate at the root - // of the chain is not self-signed (i.e. is not a root CA), then also append - // that certificate to the chain. - - // Root certificates are omitted from the chain as per - // https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 - // > [T]he self-signed certificate that specifies the root certificate authority - // > MAY be omitted from the chain, under the assumption that the remote end must - // > already possess it in order to validate it in any case. - - if c.issuer == nil { - if len(certs) > 0 && !isSelfSignedCertificate(c.cert) { - certs = append(certs, c.cert) - } - - ca = c.cert - break - } - - // Add this node's certificate to the list at the end. Ready to check - // next node up. - certs = append(certs, c.cert) - c = c.issuer - } - - caPEM, err := EncodeX509(ca) - if err != nil { - return PEMBundle{}, err - } - - // If no certificates parsed, then CA is the only certificate and should be - // the chain. If the CA is also self-signed, then by definition it's also the - // issuer and so can be placed in CAPEM too. - if len(certs) == 0 { - if isSelfSignedCertificate(ca) { - return PEMBundle{ChainPEM: caPEM, CAPEM: caPEM}, nil - } - - return PEMBundle{ChainPEM: caPEM}, nil - } - - // Encode full certificate chain - chainPEM, err := EncodeX509Chain(certs) - if err != nil { - return PEMBundle{}, err - } - - // Return chain and ca - return PEMBundle{CAPEM: caPEM, ChainPEM: chainPEM}, nil -} - -// tryMergeChain glues two chains A and B together by adding one on top of -// the other. The function tries both gluing A on top of B and B on top of -// A, which is why the argument order for the two input chains does not -// matter. -// -// Gluability: We say that the chains A and B are glueable when either the -// leaf certificate of A can be verified using the root certificate of B, -// or that the leaf certificate of B can be verified using the root certificate -// of A. -// -// A leaf certificate C (as in "child") is verified by a certificate P -// (as in "parent"), when they satisfy C.CheckSignatureFrom(P). In the -// following diagram, C.CheckSignatureFrom(P) is satisfied, i.e., the -// signature ("sig") on the certificate C can be verified using the parent P: -// -// head tail -// +------+-------+ +------+-------+ +------+-------+ -// | | | | | | | | | -// | | sig ------->| C | sig ------->| P | | -// | | | | | | | | | -// +------+-------+ +------+-------+ +------+-------+ -// leaf certificate root certificate -// -// The function returns false if the chains A and B are not gluable. -func (c *chainNode) tryMergeChain(chain *chainNode) (*chainNode, bool) { - // The given chain's root has been signed by this node. Add this node on top - // of the given chain. - if chain.root().cert.CheckSignatureFrom(c.cert) == nil { - chain.root().issuer = c - return chain, true - } - - // The given chain is the issuer of the root of this node. Add the given - // chain on top of the root of this node. - if c.root().cert.CheckSignatureFrom(chain.cert) == nil { - c.root().issuer = chain - return c, true - } - - // Chains cannot be added together. - return c, false -} - -// Return the root most node of this chain. -func (c *chainNode) root() *chainNode { - for c.issuer != nil { - c = c.issuer - } - - return c -} - -// isSelfSignedCertificate returns true if the given X.509 certificate has been -// signed by itself, which would make it a "root" certificate. -func isSelfSignedCertificate(cert *x509.Certificate) bool { - return cert.CheckSignatureFrom(cert) == nil -} diff --git a/pkg/util/pki/parse_certificate_chain.go b/pkg/util/pki/parse_certificate_chain.go new file mode 100644 index 000000000..869201c07 --- /dev/null +++ b/pkg/util/pki/parse_certificate_chain.go @@ -0,0 +1,251 @@ +/* +Copyright 2020 The cert-manager 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 pki + +import ( + "crypto/x509" + + "github.com/cert-manager/cert-manager/pkg/util/errors" +) + +// PEMBundle includes the PEM encoded X.509 certificate chain and CA. CAPEM +// contains either 1 CA certificate, or is empty if only a single certificate +// exists in the chain. +type PEMBundle struct { + CAPEM []byte + ChainPEM []byte +} + +type chainNode struct { + cert *x509.Certificate + issuer *chainNode +} + +// ParseSingleCertificateChainPEM decodes a PEM encoded certificate chain before +// calling ParseSingleCertificateChainPEM +func ParseSingleCertificateChainPEM(pembundle []byte) (PEMBundle, error) { + certs, err := DecodeX509CertificateChainBytes(pembundle) + if err != nil { + return PEMBundle{}, err + } + return ParseSingleCertificateChain(certs) +} + +// ParseSingleCertificateChain returns the PEM-encoded chain of certificates as +// well as the PEM-encoded CA certificate. +// +// The CA (CAPEM) may not be a true root, but the highest intermediate certificate. +// The certificate is chosen as follows: +// - If the chain has a self-signed root, the root certificate. +// - If the chain has no self-signed root and has > 1 certificates, the highest certificate in the chain. +// - If the chain has no self-signed root and has == 1 certificate, nil. +// +// The certificate chain (ChainPEM) starts with the leaf certificate and ends with the +// highest certificate in the chain which is not self-signed. Self-signed certificates +// are not included in the chain because we are certain they are known and trusted by the +// client already. +// +// This function removes duplicate certificate entries as well as comments and +// unnecessary white space. +// +// An error is returned if the passed bundle is not a valid single chain, +// the bundle is malformed, or the chain is broken. +func ParseSingleCertificateChain(certs []*x509.Certificate) (PEMBundle, error) { + // De-duplicate certificates. This moves "complicated" logic away from + // consumers and into a shared function, who would otherwise have to do this + // anyway. + for i := 0; i < len(certs)-1; i++ { + for j := 1; j < len(certs); j++ { + if i == j { + continue + } + if certs[i].Equal(certs[j]) { + certs = append(certs[:j], certs[j+1:]...) + } + } + } + + // A certificate chain can be well described as a linked list. Here we build + // multiple lists that contain a single node, each being a single certificate + // that was passed. + var chains []*chainNode + for i := range certs { + chains = append(chains, &chainNode{cert: certs[i]}) + } + + // The task is to build a single list which represents a single certificate + // chain. The strategy is to iteratively attempt to join items in the list to + // build this single chain. Once we have a single list, we have built the + // chain. If the number of lists do not decrease after a pass, then the list + // can never be reduced to a single chain and we error. + for { + // If a single list is left, then we have built the entire chain. Stop + // iterating. + if len(chains) == 1 { + break + } + + // lastChainsLength is used to ensure that at every pass, the number of + // tested chains gets smaller. + lastChainsLength := len(chains) + + for i := 0; i < len(chains)-1; i++ { + for j := 1; j < len(chains); j++ { + if i == j { + continue + } + + // attempt to add both chains together + chain, ok := chains[i].tryMergeChain(chains[j]) + if ok { + // If adding the chains together was successful, remove inner chain from + // list + chains = append(chains[:j], chains[j+1:]...) + } + + chains[i] = chain + } + } + + // If no chains were merged in this pass, the chain can never be built as a + // single list. Error. + if lastChainsLength == len(chains) { + return PEMBundle{}, errors.NewInvalidData("certificate chain is malformed or broken") + } + } + + // There is only a single chain left at index 0. Return chain as PEM. + return chains[0].toBundleAndCA() +} + +// toBundleAndCA will return the PEM bundle of this chain. +func (c *chainNode) toBundleAndCA() (PEMBundle, error) { + var ( + certs []*x509.Certificate + ca *x509.Certificate + ) + + for { + // If the issuer is nil, we have hit the root of the chain. Assign the CA + // to this certificate and stop traversing. If the certificate at the root + // of the chain is not self-signed (i.e. is not a root CA), then also append + // that certificate to the chain. + + // Root certificates are omitted from the chain as per + // https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 + // > [T]he self-signed certificate that specifies the root certificate authority + // > MAY be omitted from the chain, under the assumption that the remote end must + // > already possess it in order to validate it in any case. + + if c.issuer == nil { + if len(certs) > 0 && !isSelfSignedCertificate(c.cert) { + certs = append(certs, c.cert) + } + + ca = c.cert + break + } + + // Add this node's certificate to the list at the end. Ready to check + // next node up. + certs = append(certs, c.cert) + c = c.issuer + } + + caPEM, err := EncodeX509(ca) + if err != nil { + return PEMBundle{}, err + } + + // If no certificates parsed, then CA is the only certificate and should be + // the chain. If the CA is also self-signed, then by definition it's also the + // issuer and so can be placed in CAPEM too. + if len(certs) == 0 { + if isSelfSignedCertificate(ca) { + return PEMBundle{ChainPEM: caPEM, CAPEM: caPEM}, nil + } + + return PEMBundle{ChainPEM: caPEM}, nil + } + + // Encode full certificate chain + chainPEM, err := EncodeX509Chain(certs) + if err != nil { + return PEMBundle{}, err + } + + // Return chain and ca + return PEMBundle{CAPEM: caPEM, ChainPEM: chainPEM}, nil +} + +// tryMergeChain glues two chains A and B together by adding one on top of +// the other. The function tries both gluing A on top of B and B on top of +// A, which is why the argument order for the two input chains does not +// matter. +// +// Gluability: We say that the chains A and B are glueable when either the +// leaf certificate of A can be verified using the root certificate of B, +// or that the leaf certificate of B can be verified using the root certificate +// of A. +// +// A leaf certificate C (as in "child") is verified by a certificate P +// (as in "parent"), when they satisfy C.CheckSignatureFrom(P). In the +// following diagram, C.CheckSignatureFrom(P) is satisfied, i.e., the +// signature ("sig") on the certificate C can be verified using the parent P: +// +// head tail +// +------+-------+ +------+-------+ +------+-------+ +// | | | | | | | | | +// | | sig ------->| C | sig ------->| P | | +// | | | | | | | | | +// +------+-------+ +------+-------+ +------+-------+ +// leaf certificate root certificate +// +// The function returns false if the chains A and B are not gluable. +func (c *chainNode) tryMergeChain(chain *chainNode) (*chainNode, bool) { + // The given chain's root has been signed by this node. Add this node on top + // of the given chain. + if chain.root().cert.CheckSignatureFrom(c.cert) == nil { + chain.root().issuer = c + return chain, true + } + + // The given chain is the issuer of the root of this node. Add the given + // chain on top of the root of this node. + if c.root().cert.CheckSignatureFrom(chain.cert) == nil { + c.root().issuer = chain + return c, true + } + + // Chains cannot be added together. + return c, false +} + +// Return the root most node of this chain. +func (c *chainNode) root() *chainNode { + for c.issuer != nil { + c = c.issuer + } + + return c +} + +// isSelfSignedCertificate returns true if the given X.509 certificate has been +// signed by itself, which would make it a "root" certificate. +func isSelfSignedCertificate(cert *x509.Certificate) bool { + return cert.CheckSignatureFrom(cert) == nil +} diff --git a/pkg/util/pki/parse_certificate_chain_test.go b/pkg/util/pki/parse_certificate_chain_test.go new file mode 100644 index 000000000..b38f753c6 --- /dev/null +++ b/pkg/util/pki/parse_certificate_chain_test.go @@ -0,0 +1,208 @@ +/* +Copyright 2020 The cert-manager 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 pki + +import ( + "crypto" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "reflect" + "testing" + "time" +) + +type testBundle struct { + cert *x509.Certificate + pem []byte + pk crypto.PrivateKey +} + +func mustCreateBundle(t *testing.T, issuer *testBundle, name string) *testBundle { + pk, err := GenerateECPrivateKey(256) + if err != nil { + t.Fatal(err) + } + + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + t.Fatal(err) + } + + template := &x509.Certificate{ + Version: 3, + BasicConstraintsValid: true, + SerialNumber: serialNumber, + PublicKeyAlgorithm: x509.ECDSA, + PublicKey: pk.Public(), + IsCA: true, + Subject: pkix.Name{ + CommonName: name, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(time.Minute), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + } + + var ( + issuerKey crypto.PrivateKey + issuerCert *x509.Certificate + ) + + if issuer == nil { + // No issuer implies the cert should be self signed + issuerKey = pk + issuerCert = template + } else { + issuerKey = issuer.pk + issuerCert = issuer.cert + } + + certPEM, cert, err := SignCertificate(template, issuerCert, pk.Public(), issuerKey) + if err != nil { + t.Fatal(err) + } + + return &testBundle{pem: certPEM, cert: cert, pk: pk} +} + +func joinPEM(first []byte, rest ...[]byte) []byte { + for _, b := range rest { + first = append(first, b...) + } + + return first +} + +func TestParseSingleCertificateChain(t *testing.T) { + root := mustCreateBundle(t, nil, "root") + intA1 := mustCreateBundle(t, root, "intA-1") + intA2 := mustCreateBundle(t, intA1, "intA-2") + intB1 := mustCreateBundle(t, root, "intB-1") + intB2 := mustCreateBundle(t, intB1, "intB-2") + leaf := mustCreateBundle(t, intA2, "leaf") + leafInterCN := mustCreateBundle(t, intA2, intA2.cert.Subject.CommonName) + random := mustCreateBundle(t, nil, "random") + + tests := map[string]struct { + inputBundle []byte + expPEMBundle PEMBundle + expErr bool + }{ + "if two certificate chain passed in order, should return single ca and certificate": { + inputBundle: joinPEM(intA1.pem, root.pem), + expPEMBundle: PEMBundle{ChainPEM: intA1.pem, CAPEM: root.pem}, + expErr: false, + }, + "if two certificate chain passed with leaf and intermediate, should return both certs in chain with intermediate as CA": { + inputBundle: joinPEM(leaf.pem, intA2.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem), CAPEM: intA2.pem}, + expErr: false, + }, + "if two certificate chain passed out of order, should return single ca and certificate": { + inputBundle: joinPEM(root.pem, intA1.pem), + expPEMBundle: PEMBundle{ChainPEM: intA1.pem, CAPEM: root.pem}, + expErr: false, + }, + "if 3 certificate chain passed out of order, should return single ca and chain in order": { + inputBundle: joinPEM(root.pem, intA2.pem, intA1.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "empty entries should be ignored, and return ca and certificate": { + inputBundle: joinPEM(root.pem, intA2.pem, []byte("\n#foo\n \n"), intA1.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if 4 certificate chain passed in order, should return single ca and chain in order": { + inputBundle: joinPEM(leaf.pem, intA1.pem, intA2.pem, root.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if certificate chain has two certs with the same CN, shouldn't affect output": { + // see https://github.com/cert-manager/cert-manager/issues/4142 + inputBundle: joinPEM(leafInterCN.pem, intA1.pem, intA2.pem, root.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leafInterCN.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if 4 certificate chain passed out of order, should return single ca and chain in order": { + inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem, intA2.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if 3 certificate chain but has break in the chain, should return error": { + inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem), + expPEMBundle: PEMBundle{}, + expErr: true, + }, + "if 4 certificate chain but also random certificate, should return error": { + inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem, intA2.pem, random.pem), + expPEMBundle: PEMBundle{}, + expErr: true, + }, + "if 6 certificate chain but some are duplicates, duplicates should be removed and return single ca with chain": { + inputBundle: joinPEM(intA2.pem, intA1.pem, root.pem, leaf.pem, intA1.pem, root.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if 6 certificate chain in different configuration but some are duplicates, duplicates should be removed and return single ca with chain": { + inputBundle: joinPEM(root.pem, intA1.pem, intA2.pem, leaf.pem, root.pem, intA1.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, + expErr: false, + }, + "if certificate chain contains branches, then should error": { + inputBundle: joinPEM(root.pem, intA1.pem, intA2.pem, intB1.pem, intB2.pem), + expPEMBundle: PEMBundle{}, + expErr: true, + }, + "if certificate chain does not have a root ca, should append all intermediates to ChainPEM and use the root-most cert as CAPEM": { + inputBundle: joinPEM(intA1.pem, intA2.pem, leaf.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: intA1.pem}, + expErr: false, + }, + "if only a single leaf certificate was parsed, ChainPEM should contain a single leaf certificate and CAPEM should remain empty": { + inputBundle: joinPEM(leaf.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem), CAPEM: nil}, + expErr: false, + }, + "if only a single intermediate certificate was parsed, ChainPEM should contain a single intermediate certificate and CAPEM should remain empty": { + inputBundle: joinPEM(intA1.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA1.pem), CAPEM: nil}, + expErr: false, + }, + "if only a single root certificate was parsed, ChainPEM should contain a single root certificate and CAPEM should also contain that root": { + inputBundle: joinPEM(root.pem), + expPEMBundle: PEMBundle{ChainPEM: joinPEM(root.pem), CAPEM: root.pem}, + expErr: false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + bundle, err := ParseSingleCertificateChainPEM(test.inputBundle) + if (err != nil) != test.expErr { + t.Errorf("unexpected error, exp=%t got=%v", + test.expErr, err) + } + + if !reflect.DeepEqual(bundle, test.expPEMBundle) { + t.Errorf("unexpected pem bundle, exp=%+s got=%+s", + test.expPEMBundle, bundle) + } + }) + } +} diff --git a/pkg/util/pki/parse_test.go b/pkg/util/pki/parse_test.go index a5ed48368..a219564c4 100644 --- a/pkg/util/pki/parse_test.go +++ b/pkg/util/pki/parse_test.go @@ -17,18 +17,13 @@ limitations under the License. package pki import ( - "crypto" "crypto/ecdsa" - "crypto/rand" "crypto/rsa" - "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" - "reflect" "strings" "testing" - "time" v1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" "github.com/stretchr/testify/assert" @@ -183,187 +178,6 @@ func TestDecodePrivateKeyBytes(t *testing.T) { } } -type testBundle struct { - cert *x509.Certificate - pem []byte - pk crypto.PrivateKey -} - -func mustCreateBundle(t *testing.T, issuer *testBundle, name string) *testBundle { - pk, err := GenerateECPrivateKey(256) - if err != nil { - t.Fatal(err) - } - - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - t.Fatal(err) - } - - template := &x509.Certificate{ - Version: 3, - BasicConstraintsValid: true, - SerialNumber: serialNumber, - PublicKeyAlgorithm: x509.ECDSA, - PublicKey: pk.Public(), - IsCA: true, - Subject: pkix.Name{ - CommonName: name, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(time.Minute), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - } - - var ( - issuerKey crypto.PrivateKey - issuerCert *x509.Certificate - ) - - if issuer == nil { - // No issuer implies the cert should be self signed - issuerKey = pk - issuerCert = template - } else { - issuerKey = issuer.pk - issuerCert = issuer.cert - } - - certPEM, cert, err := SignCertificate(template, issuerCert, pk.Public(), issuerKey) - if err != nil { - t.Fatal(err) - } - - return &testBundle{pem: certPEM, cert: cert, pk: pk} -} - -func joinPEM(first []byte, rest ...[]byte) []byte { - for _, b := range rest { - first = append(first, b...) - } - - return first -} - -func TestParseSingleCertificateChain(t *testing.T) { - root := mustCreateBundle(t, nil, "root") - intA1 := mustCreateBundle(t, root, "intA-1") - intA2 := mustCreateBundle(t, intA1, "intA-2") - intB1 := mustCreateBundle(t, root, "intB-1") - intB2 := mustCreateBundle(t, intB1, "intB-2") - leaf := mustCreateBundle(t, intA2, "leaf") - leafInterCN := mustCreateBundle(t, intA2, intA2.cert.Subject.CommonName) - random := mustCreateBundle(t, nil, "random") - - tests := map[string]struct { - inputBundle []byte - expPEMBundle PEMBundle - expErr bool - }{ - "if two certificate chain passed in order, should return single ca and certificate": { - inputBundle: joinPEM(intA1.pem, root.pem), - expPEMBundle: PEMBundle{ChainPEM: intA1.pem, CAPEM: root.pem}, - expErr: false, - }, - "if two certificate chain passed with leaf and intermediate, should return both certs in chain with intermediate as CA": { - inputBundle: joinPEM(leaf.pem, intA2.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem), CAPEM: intA2.pem}, - expErr: false, - }, - "if two certificate chain passed out of order, should return single ca and certificate": { - inputBundle: joinPEM(root.pem, intA1.pem), - expPEMBundle: PEMBundle{ChainPEM: intA1.pem, CAPEM: root.pem}, - expErr: false, - }, - "if 3 certificate chain passed out of order, should return single ca and chain in order": { - inputBundle: joinPEM(root.pem, intA2.pem, intA1.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "empty entries should be ignored, and return ca and certificate": { - inputBundle: joinPEM(root.pem, intA2.pem, []byte("\n#foo\n \n"), intA1.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if 4 certificate chain passed in order, should return single ca and chain in order": { - inputBundle: joinPEM(leaf.pem, intA1.pem, intA2.pem, root.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if certificate chain has two certs with the same CN, shouldn't affect output": { - // see https://github.com/cert-manager/cert-manager/issues/4142 - inputBundle: joinPEM(leafInterCN.pem, intA1.pem, intA2.pem, root.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leafInterCN.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if 4 certificate chain passed out of order, should return single ca and chain in order": { - inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem, intA2.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if 3 certificate chain but has break in the chain, should return error": { - inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem), - expPEMBundle: PEMBundle{}, - expErr: true, - }, - "if 4 certificate chain but also random certificate, should return error": { - inputBundle: joinPEM(root.pem, intA1.pem, leaf.pem, intA2.pem, random.pem), - expPEMBundle: PEMBundle{}, - expErr: true, - }, - "if 6 certificate chain but some are duplicates, duplicates should be removed and return single ca with chain": { - inputBundle: joinPEM(intA2.pem, intA1.pem, root.pem, leaf.pem, intA1.pem, root.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if 6 certificate chain in different configuration but some are duplicates, duplicates should be removed and return single ca with chain": { - inputBundle: joinPEM(root.pem, intA1.pem, intA2.pem, leaf.pem, root.pem, intA1.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: root.pem}, - expErr: false, - }, - "if certificate chain contains branches, then should error": { - inputBundle: joinPEM(root.pem, intA1.pem, intA2.pem, intB1.pem, intB2.pem), - expPEMBundle: PEMBundle{}, - expErr: true, - }, - "if certificate chain does not have a root ca, should append all intermediates to ChainPEM and use the root-most cert as CAPEM": { - inputBundle: joinPEM(intA1.pem, intA2.pem, leaf.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem, intA2.pem, intA1.pem), CAPEM: intA1.pem}, - expErr: false, - }, - "if only a single leaf certificate was parsed, ChainPEM should contain a single leaf certificate and CAPEM should remain empty": { - inputBundle: joinPEM(leaf.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(leaf.pem), CAPEM: nil}, - expErr: false, - }, - "if only a single intermediate certificate was parsed, ChainPEM should contain a single intermediate certificate and CAPEM should remain empty": { - inputBundle: joinPEM(intA1.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(intA1.pem), CAPEM: nil}, - expErr: false, - }, - "if only a single root certificate was parsed, ChainPEM should contain a single root certificate and CAPEM should also contain that root": { - inputBundle: joinPEM(root.pem), - expPEMBundle: PEMBundle{ChainPEM: joinPEM(root.pem), CAPEM: root.pem}, - expErr: false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - bundle, err := ParseSingleCertificateChainPEM(test.inputBundle) - if (err != nil) != test.expErr { - t.Errorf("unexpected error, exp=%t got=%v", - test.expErr, err) - } - - if !reflect.DeepEqual(bundle, test.expPEMBundle) { - t.Errorf("unexpected pem bundle, exp=%+s got=%+s", - test.expPEMBundle, bundle) - } - }) - } -} - func TestMustParseRDN(t *testing.T) { subject := "SERIALNUMBER=42, L=some-locality, ST=some-state-or-province, STREET=some-street, CN=foo-long.com, OU=FooLong, OU=Barq, OU=Baz, OU=Dept., O=Corp., C=US" rdnSeq, err := UnmarshalSubjectStringToRDNSequence(subject)