diff --git a/pkg/util/pki/parse.go b/pkg/util/pki/parse.go index a24accb7b..3958e10e7 100644 --- a/pkg/util/pki/parse.go +++ b/pkg/util/pki/parse.go @@ -256,10 +256,17 @@ func (c *chainNode) toBundleAndCA() (PEMBundle, error) { // 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 !isSelfSignedCertificate(c.cert) { + if len(certs) > 0 && !isSelfSignedCertificate(c.cert) { certs = append(certs, c.cert) } + ca = c.cert break } @@ -276,8 +283,13 @@ func (c *chainNode) toBundleAndCA() (PEMBundle, error) { } // If no certificates parsed, then CA is the only certificate and should be - // the chain + // 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 } diff --git a/pkg/util/pki/parse_test.go b/pkg/util/pki/parse_test.go index 52fc85b2f..e874964d2 100644 --- a/pkg/util/pki/parse_test.go +++ b/pkg/util/pki/parse_test.go @@ -258,16 +258,16 @@ func TestParseSingleCertificateChain(t *testing.T) { expPEMBundle PEMBundle expErr bool }{ - "if single certificate passed, return single certificate": { - inputBundle: root.pem, - expPEMBundle: PEMBundle{ChainPEM: root.pem}, - expErr: false, - }, "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}, @@ -329,6 +329,21 @@ func TestParseSingleCertificateChain(t *testing.T) { 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 {