From 132fa87bf36b3cbddc4ab880570d3dc20ed10377 Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Thu, 14 May 2026 11:45:21 -0400 Subject: [PATCH] fix: only embed Azure roots on darwin (#25312) Partially reverts #25136 for non-darwin platforms. In general we want to avoid pinning trust roots to embedded Certs, since that limits operational flexibility. If Azure changes CAs, operators should, at most, be able to update the OS trust store to keep Coder working correctly. Embedding roots means we need to upgrade the Coder binary. Since Coder Server on macOS is not really supported for production use, embedding only in that case to ease development and testing is OK. --- .github/workflows/typos.toml | 2 + coderd/azureidentity/azureidentity.go | 102 +--------------- coderd/azureidentity/azureidentity_test.go | 38 ------ coderd/azureidentity/roots_darwin.go | 111 ++++++++++++++++++ .../roots_darwin_internal_test.go | 45 +++++++ coderd/azureidentity/roots_other.go | 10 ++ 6 files changed, 171 insertions(+), 137 deletions(-) create mode 100644 coderd/azureidentity/roots_darwin.go create mode 100644 coderd/azureidentity/roots_darwin_internal_test.go create mode 100644 coderd/azureidentity/roots_other.go diff --git a/.github/workflows/typos.toml b/.github/workflows/typos.toml index fd962da6dc..6cc06617da 100644 --- a/.github/workflows/typos.toml +++ b/.github/workflows/typos.toml @@ -54,6 +54,8 @@ extend-exclude = [ "tailnet/testdata/**", "site/src/pages/SetupPage/countries.tsx", "provisioner/terraform/testdata/**", + "coderd/azureidentity/roots_darwin.go", + "coderd/azureidentity/azureidentity.go", # notifications' golden files confuse the detector because of quoted-printable encoding "coderd/notifications/testdata/**", "agent/agentcontainers/testdata/devcontainercli/**", diff --git a/coderd/azureidentity/azureidentity.go b/coderd/azureidentity/azureidentity.go index cf7756826e..eb451a0a53 100644 --- a/coderd/azureidentity/azureidentity.go +++ b/coderd/azureidentity/azureidentity.go @@ -184,7 +184,9 @@ type metadata struct { type Options struct { // Roots is the trusted root certificate pool. If nil, - // the embedded root certificate pool is used. + // the default cert pool is used. On darwin, this is an + // embedded pool. On all other platforms it is the system + // pool. Roots *x509.CertPool // Intermediates are additional intermediate certificates to // inject into the PKCS7 object for chain verification. Azure @@ -335,104 +337,6 @@ func ParseCertificates() ([]*x509.Certificate, error) { return certs, nil } -// Roots are the root CAs that Azure instance-identity certificates chain to. -// These are embedded so verification works deterministically on all -// platforms, including macOS where the system verifier would otherwise be -// used and may reject otherwise valid Azure certificates due to stricter -// standards-compliance checks. See https://github.com/coder/coder/issues/12978. -var Roots = []string{ - // DigiCert Global Root G2 - `-----BEGIN CERTIFICATE----- -MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 -d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH -MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j -b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG -9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI -2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx -1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ -q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz -tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ -vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP -BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV -5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY -1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 -NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG -Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 -8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe -pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl -MrY= ------END CERTIFICATE-----`, - // DigiCert Global Root G3 - `-----BEGIN CERTIFICATE----- -MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw -CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu -ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe -Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw -EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x -IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF -K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG -fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO -Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd -BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx -AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ -oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 -sycX ------END CERTIFICATE-----`, - // Baltimore CyberTrust Root. - // Required for chains rooted here, e.g. "Microsoft RSA TLS CA 01/02". - // Expired 2025-05-12 but kept so callers that pass a CurrentTime - // before the expiry can still verify historical signatures. - `-----BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ -RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD -VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX -DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y -ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy -VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr -mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr -IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK -mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu -XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy -dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye -jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 -BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 -DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 -9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx -jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 -Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz -ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS -R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE-----`, -} - -// rootCertPool returns a CertPool containing the root CAs that Azure -// instance-identity certificates ultimately chain to. We embed these so -// callers do not have to populate Roots themselves, and so we never -// implicitly fall back to the platform's system verifier (notably Apple's -// Security framework on macOS/iOS) which enforces stricter standards- -// compliance checks than Go's pure-Go verifier and rejects some otherwise -// valid Azure leaf certificates. -var rootCertPool = sync.OnceValues(func() (*x509.CertPool, error) { - pool := x509.NewCertPool() - for _, pemCert := range Roots { - block, rest := pem.Decode([]byte(pemCert)) - if block == nil { - return nil, xerrors.New("root: failed to decode PEM block") - } - if len(rest) != 0 { - return nil, xerrors.Errorf("root: invalid certificate, %d bytes remain", len(rest)) - } - cert, err := x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, xerrors.Errorf("root: parse certificate: %w", err) - } - pool.AddCert(cert) - } - return pool, nil -}) - // Certificates are manually downloaded from Azure, then processed with OpenSSL // and added here. See: https://learn.microsoft.com/en-us/azure/security/fundamentals/azure-ca-details // diff --git a/coderd/azureidentity/azureidentity_test.go b/coderd/azureidentity/azureidentity_test.go index f5138e64ac..04e2f003cd 100644 --- a/coderd/azureidentity/azureidentity_test.go +++ b/coderd/azureidentity/azureidentity_test.go @@ -8,7 +8,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/base64" - "encoding/pem" "math/big" "runtime" "testing" @@ -62,40 +61,6 @@ func TestValidate(t *testing.T) { } } -// TestEmbeddedRoots ensures the package's embedded root certificates parse -// successfully. The Roots are used by Validate to avoid falling back to the -// platform's system verifier (notably Apple's Security framework on macOS), -// which previously caused TestValidate/regular to fail on macOS with -// `x509: "metadata.azure.com" certificate is not standards compliant`. -// See https://github.com/coder/coder/issues/12978. -func TestEmbeddedRoots(t *testing.T) { - t.Parallel() - require.NotEmpty(t, azureidentity.Roots, "embedded roots must not be empty") - seen := map[string]bool{} - for _, pemCert := range azureidentity.Roots { - block, rest := pem.Decode([]byte(pemCert)) - require.NotNil(t, block, "PEM block should decode") - require.Zero(t, len(rest), "no trailing data after PEM block") - cert, err := x509.ParseCertificate(block.Bytes) - require.NoError(t, err) - // Each root must be self-signed (issuer == subject). - require.Equal(t, cert.Issuer.String(), cert.Subject.String(), - "root certificate must be self-signed: %s", cert.Subject.CommonName) - require.False(t, seen[cert.Subject.CommonName], - "duplicate embedded root: %s", cert.Subject.CommonName) - seen[cert.Subject.CommonName] = true - } - // Verify the three roots Azure instance-identity chains ultimately - // terminate at are all present. - for _, name := range []string{ - "DigiCert Global Root G2", - "DigiCert Global Root G3", - "Baltimore CyberTrust Root", - } { - require.True(t, seen[name], "missing embedded root %q", name) - } -} - func TestExpiresSoon(t *testing.T) { t.Parallel() // TODO (@kylecarbs): It's unknown why Microsoft does not have new certificates live... @@ -261,9 +226,6 @@ func (tc *testCertChain) validationOptions() azureidentity.Options { func TestValidate_TamperedContent(t *testing.T) { t.Parallel() - if runtime.GOOS == "darwin" { - t.Skip("pkcs7 signing uses SHA1 which may be restricted on macOS") - } chain := newTestCertChain(t) diff --git a/coderd/azureidentity/roots_darwin.go b/coderd/azureidentity/roots_darwin.go new file mode 100644 index 0000000000..edf6bfcfb7 --- /dev/null +++ b/coderd/azureidentity/roots_darwin.go @@ -0,0 +1,111 @@ +//go:build darwin + +package azureidentity + +import ( + "crypto/x509" + "encoding/pem" + "sync" + + "golang.org/x/xerrors" +) + +// rootCertPool returns a CertPool containing the root CAs that Azure +// instance-identity certificates ultimately chain to. On macOS, we embed these +// because Apple's Security framework enforces stricter standards-compliance +// checks than Go's pure-Go verifier and rejects some otherwise valid Azure leaf +// certificates. However, we want to avoid hardcoding the roots on other +// platforms because if Azure changes their root CAs, we want operators to be +// able to validate without having to get a new Coder binary. macOS support for +// coderd is only intended for development and testing, so this is a small trade +// off. +var rootCertPool = sync.OnceValues(func() (*x509.CertPool, error) { + pool := x509.NewCertPool() + for _, pemCert := range embeddedRoots { + block, rest := pem.Decode([]byte(pemCert)) + if block == nil { + return nil, xerrors.New("root: failed to decode PEM block") + } + if len(rest) != 0 { + return nil, xerrors.Errorf("root: invalid certificate, %d bytes remain", len(rest)) + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, xerrors.Errorf("root: parse certificate: %w", err) + } + pool.AddCert(cert) + } + return pool, nil +}) + +// embeddedRoots are the root CAs that Azure instance-identity certificates +// chain to. These are embedded so verification works on macOS where the system +// verifier would otherwise be used and may reject otherwise valid Azure +// certificates due to stricter standards-compliance checks. +// See https://github.com/coder/coder/issues/12978. +var embeddedRoots = []string{ + // DigiCert Global Root G2 + `-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE-----`, + // DigiCert Global Root G3 + `-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE-----`, + // Baltimore CyberTrust Root. + // Required for chains rooted here, e.g. "Microsoft RSA TLS CA 01/02". + // Expired 2025-05-12 but kept so callers that pass a CurrentTime + // before the expiry can still verify historical signatures. + `-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ +RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD +VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX +DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y +ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy +VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr +mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr +IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK +mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu +XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy +dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye +jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 +BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 +DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 +9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx +jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 +Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz +ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS +R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp +-----END CERTIFICATE-----`, +} diff --git a/coderd/azureidentity/roots_darwin_internal_test.go b/coderd/azureidentity/roots_darwin_internal_test.go new file mode 100644 index 0000000000..461c43465b --- /dev/null +++ b/coderd/azureidentity/roots_darwin_internal_test.go @@ -0,0 +1,45 @@ +//go:build darwin + +package azureidentity + +import ( + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestEmbeddedRoots ensures the package's embedded root certificates parse +// successfully. The roots are used by Validate to avoid falling back to the +// platform's system verifier (notably Apple's Security framework on macOS), +// which previously caused TestValidate/regular to fail on macOS with +// `x509: "metadata.azure.com" certificate is not standards compliant`. +// See https://github.com/coder/coder/issues/12978. +func TestEmbeddedRoots(t *testing.T) { + t.Parallel() + require.NotEmpty(t, embeddedRoots, "embedded roots must not be empty") + seen := map[string]bool{} + for _, pemCert := range embeddedRoots { + block, rest := pem.Decode([]byte(pemCert)) + require.NotNil(t, block, "PEM block should decode") + require.Zero(t, len(rest), "no trailing data after PEM block") + cert, err := x509.ParseCertificate(block.Bytes) + require.NoError(t, err) + // Each root must be self-signed (issuer == subject). + require.Equal(t, cert.Issuer.String(), cert.Subject.String(), + "root certificate must be self-signed: %s", cert.Subject.CommonName) + require.False(t, seen[cert.Subject.CommonName], + "duplicate embedded root: %s", cert.Subject.CommonName) + seen[cert.Subject.CommonName] = true + } + // Verify the three roots Azure instance-identity chains ultimately + // terminate at are all present. + for _, name := range []string{ + "DigiCert Global Root G2", + "DigiCert Global Root G3", + "Baltimore CyberTrust Root", + } { + require.True(t, seen[name], "missing embedded root %q", name) + } +} diff --git a/coderd/azureidentity/roots_other.go b/coderd/azureidentity/roots_other.go new file mode 100644 index 0000000000..d12731f2d8 --- /dev/null +++ b/coderd/azureidentity/roots_other.go @@ -0,0 +1,10 @@ +//go:build !darwin + +package azureidentity + +import "crypto/x509" + +// rootCertPool returns the system cert pool on non-Apple platforms. +func rootCertPool() (*x509.CertPool, error) { + return x509.SystemCertPool() +}