diff --git a/pemutil/pem.go b/pemutil/pem.go index b281c402..c1d28931 100644 --- a/pemutil/pem.go +++ b/pemutil/pem.go @@ -740,3 +740,42 @@ func BundleCertificate(bundlePEM []byte, certsPEM ...[]byte) ([]byte, bool, erro return bundlePEM, modified, nil } + +// UnbundleCertificate removes PEM-encoded certificates from a PEM-encoded +// certificate bundle. +func UnbundleCertificate(bundlePEM []byte, certsPEM ...[]byte) ([]byte, bool, error) { + if len(certsPEM) == 0 { + return bundlePEM, false, nil + } + drop := make(map[[sha256.Size224]byte]bool, len(certsPEM)) + for i := range certsPEM { + certs, err := ParseCertificateBundle(certsPEM[i]) + if err != nil { + return nil, false, fmt.Errorf("invalid certificate %d: %w", i, err) + } + for _, cert := range certs { + drop[sha256.Sum224(cert.Raw)] = true + } + } + + var modified bool + var keep []byte + + bundle, err := ParseCertificateBundle(bundlePEM) + if err != nil { + return nil, false, fmt.Errorf("invalid bundle: %w", err) + } + for _, cert := range bundle { + sum := sha256.Sum224(cert.Raw) + if drop[sum] { + modified = true + continue + } + keep = append(keep, pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + })...) + } + + return keep, modified, nil +} diff --git a/pemutil/pem_test.go b/pemutil/pem_test.go index 8a22c40c..73c6b557 100644 --- a/pemutil/pem_test.go +++ b/pemutil/pem_test.go @@ -1225,3 +1225,57 @@ func TestBundleCertificate(t *testing.T) { }) } } + +func TestUnbundleCertificate(t *testing.T) { + tests := []struct { + name string + bundle string + certs []string + wantBundle string + modified bool + err error + }{ + {"remove one leave one", "testdata/bundle.crt", []string{"testdata/bundle-1st.crt"}, "testdata/bundle-2nd.crt", true, nil}, + {"remove two leave none", "testdata/bundle.crt", []string{"testdata/bundle-1st.crt", "testdata/bundle-2nd.crt"}, "", true, nil}, + {"remove none", "testdata/bundle.crt", []string{"testdata/ca.crt"}, "testdata/bundle.crt", false, nil}, + {"none to remove", "testdata/bundle.crt", []string{}, "testdata/bundle.crt", false, nil}, + {"remove bundle", "testdata/bundle.crt", []string{"testdata/bundle.crt"}, "", true, nil}, + {"bad cert", "testdata/bundle.crt", []string{"testdata/badca.crt"}, "", false, errors.New("invalid certificate 0")}, + {"bad bundle", "testdata/badca.crt", []string{"testdata/ca.crt"}, "", false, errors.New("invalid bundle")}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + bundlePEM, err := os.ReadFile(tc.bundle) + if err != nil { + t.Fatal(err) + } + certsPEM := make([][]byte, len(tc.certs)) + for i, fn := range tc.certs { + certsPEM[i], err = os.ReadFile(fn) + if err != nil { + t.Fatal(err) + } + } + + got, modified, err := UnbundleCertificate(bundlePEM, certsPEM...) + if tc.err != nil { + if assert.Error(t, err) { + assert.HasPrefix(t, err.Error(), tc.err.Error()) + } + } else { + assert.NoError(t, err) + assert.Equals(t, tc.modified, modified) + if tc.wantBundle == "" { + assert.Nil(t, got) + } else { + want, err := os.ReadFile(tc.wantBundle) + if err != nil { + t.Fatal(err) + } + assert.Equals(t, strings.TrimSpace(string(want)), strings.TrimSpace(string(got))) + } + } + }) + } +} diff --git a/pemutil/testdata/bundle-1st.crt b/pemutil/testdata/bundle-1st.crt new file mode 100644 index 00000000..4517e440 --- /dev/null +++ b/pemutil/testdata/bundle-1st.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEhzCCA2+gAwIBAgISA78mVnMzLbLQxw5IoWP7fRG6MA0GCSqGSIb3DQEBCwUA +MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD +ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTAyMDgxMzA3NDRaFw0x +OTA1MDkxMzA3NDRaMBgxFjAUBgNVBAMTDXNtYWxsc3RlcC5jb20wWTATBgcqhkjO +PQIBBggqhkjOPQMBBwNCAATtaDvEhLijnzgpf/svy2v0lA0q1KNMmKmb8kdIgFsi +Rqmzh0IPldiprW6/zIBPKC3ZWBzdw06ZuSXeuPQ0rcC1o4ICYjCCAl4wDgYDVR0P +AQH/BAQDAgeAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMB +Af8EAjAAMB0GA1UdDgQWBBQ5p9apFolkDFuITyFnBK4BxE67dDAfBgNVHSMEGDAW +gBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEwLgYIKwYBBQUH +MAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcwLwYIKwYBBQUH +MAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcvMBgGA1UdEQQR +MA+CDXNtYWxsc3RlcC5jb20wTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC +3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcw +ggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQB0ftqDMa0zEJEhnM4lT0Jwwr/9XkIg +CMY3NXnmEHvMVgAAAWjNb4RTAAAEAwBGMEQCID7NdufkWtiID0FJKcXBiUnhW1OX +w2eU1ZRsitnaRqL3AiBlGOiUaaWf92NGqlEkEp2/oaED0OZYbLe1LTvPnRsQoAB3 +AGPy283oO8wszwtyhCdXazOkjWF3j711pjixx2hUS9iNAAABaM1vhI4AAAQDAEgw +RgIhAJ8A7OHfNThbzUOiSk5Y+JOSvOiSJ1ferIOX4z3AbD7qAiEA3Aiw5ZfrXyEn +PsHWofgMuz8dWvv4QxFXxLZRmXH0QDIwDQYJKoZIhvcNAQELBQADggEBAFrmkLMe +OhGGuOSkY3hsUnSEUy5N1lrpGRrwyWVHTPcLJdlds5S8l5xYg2LcPfWQXkUHUYcr +Fo7jT5Up4UIXYvE6Lctm48geIExlQwcOkSo3ULSQJYz9bp1tDpv9cQgyHJtwfrbR +2rxtpasLIs8znzbBcJlQ4rlodyzUMEJh8YgT9XpynDbk5K43nfsng1uRqI9J6brt +AasWcqPaJ97ILTT3DNtk2cLBpAqtMwaxcROdZ1104fbWzYjGgv67W78CBgndhvbp +Yx8h05Bm4vY0tz7Zv0Qd3YwFKgIZQI/BR/Mdber9P+xYU51T6xu4p4JDcQsCxtYg +9zBQ7U7V9X22RGo= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pemutil/testdata/bundle-2nd.crt b/pemutil/testdata/bundle-2nd.crt new file mode 100644 index 00000000..edb593bc --- /dev/null +++ b/pemutil/testdata/bundle-2nd.crt @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ +MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT +DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow +SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT +GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF +q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 +SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 +Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA +a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj +/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T +AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG +CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv +bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k +c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw +VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC +ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz +MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu +Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF +AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo +uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ +wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu +X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG +PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 +KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== +-----END CERTIFICATE----- \ No newline at end of file