package evaluator import ( "encoding/asn1" "regexp" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pomerium/pomerium/config" ) // These certificates can be regenerated by running: // // go run ./gen-test-certs.go // // (Copy and paste the output here.) const ( testCA = ` -----BEGIN CERTIFICATE----- MIIBaDCCAQ6gAwIBAgICEAAwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAaMRgw FgYDVQQDEw9UcnVzdGVkIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC AAS+oiuwekZ86TUjhJQV12ZjAlt+3Zy/VkRuj7tA7wtFwEs8w77iQryIQO/DEccY 9coUjLfWFc/V5LTsNlYTh4B8o0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ BAUwAwEB/zAdBgNVHQ4EFgQU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwCgYIKoZIzj0E AwIDSAAwRQIhAMH+UKlJxWDdtKH7YRnhSefk/AxfjdjrDJhQKm4EUrVjAiAxxvdP yoEpPAq1NW/Ny9yuKE8mfRTWgu+09L3jOwDqTg== -----END CERTIFICATE----- ` testValidCert = ` -----BEGIN CERTIFICATE----- MIIBYTCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAeMRww GgYDVQQDExN0cnVzdGVkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAED3L4Hf7kBaa6E76kRaideTYPS+deD+T1+qwfD5amUF4h3dblJCQgDuWl p9WA7PzJioroP4HeUUVll8GF4Ngx/aM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw HwYDVR0jBBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwCgYIKoZIzj0EAwIDRwAw RAIgeS3o3PnBZTGSM5yFuxl+xZQwItUGOvk4TUIHbLKIh+QCIGYABXVpXyaHT1/7 xAdvB6E8eNv4/LIbPo3OKVwzLWka -----END CERTIFICATE----- ` testUntrustedCert = ` -----BEGIN CERTIFICATE----- MIIBZzCCAQygAwIBAgICEAEwCgYIKoZIzj0EAwIwHDEaMBgGA1UEAxMRVW50cnVz dGVkIFJvb3QgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zNDA3MjMxODA3NDlaMCAx HjAcBgNVBAMTFXVudHJ1c3RlZCBjbGllbnQgY2VydDBZMBMGByqGSM49AgEGCCqG SM49AwEHA0IABD7wlyIZI0dk81W93CPi0C9EK5oWnP9jyf6ukNSfuN2/AifVyskZ ZaJsC1y0x11eHQn0AKDvFZvrM5ntgtEKvTujODA2MBMGA1UdJQQMMAoGCCsGAQUF BwMCMB8GA1UdIwQYMBaAFCWSSHqA46gKGwIZH3RAYyds6hJ0MAoGCCqGSM49BAMC A0kAMEYCIQDeg+lSgmzg5W85HpMjnfcbDxOwTFAuG2dBZIFm4MeZRAIhAOhReIDb B0ktSaPCNQOn1m0PMi4OxnSrClWV0pQzZwiM -----END CERTIFICATE----- ` testRevokedCert = ` -----BEGIN CERTIFICATE----- MIIBYzCCAQigAwIBAgICEAIwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAeMRww GgYDVQQDExNyZXZva2VkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D AQcDQgAE5sRZhAupCq3X0KWDNRfJh0H0jOGNOQawaKFejCQQ0t2kXCvfkvcTlGWo Cjlgtc885wLI5n0KPG5ugN1Zk7nrlqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw HwYDVR0jBBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwCgYIKoZIzj0EAwIDSQAw RgIhAIzYBLOWFTLdWvf//svgjUAtjW51/qvuR+oUQSpyJEs7AiEAlmGwYPnUpVah 1ri42dcuvYe6u+E7yWQOvF9MRVBQFrI= -----END CERTIFICATE----- ` testCRL = ` -----BEGIN X509 CRL----- MIHeMIGFAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD1RydXN0ZWQgUm9vdCBD QRgPMDAwMTAxMDEwMDAwMDBaMBUwEwICEAIXDTI0MDcyNTE4MDc0OVqgMDAuMB8G A1UdIwQYMBaAFOIUzZsarER1GfDZP5tN0R6OhxsgMAsGA1UdFAQEAgIgADAKBggq hkjOPQQDAgNIADBFAiAMtFj+hNOleIXozxi6QJJrRtS/7LyYrDuRju/CKvbxhwIh AKXWklJEPCyzzhIt4ETiesXhMu8UUngRhuWwGLFQoyJH -----END X509 CRL----- ` testIntermediateCA = ` -----BEGIN CERTIFICATE----- MIIBkTCCATegAwIBAgICEAMwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAiMSAw HgYDVQQDExdUcnVzdGVkIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqG SM49AwEHA0IABCSif6O54Y74VpVNMHl37iECM1RKCjzJgu7a3CE2O8W7IdO3vQVT X4FTcGjzR+WaQNdssuAJ8ch5lxDbOQDPT4WjYzBhMA4GA1UdDwEB/wQEAwIBBjAP BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTlu+78yKKKOakqZzrIgHoWLRMc/TAf BgNVHSMEGDAWgBTiFM2bGqxEdRnw2T+bTdEejocbIDAKBggqhkjOPQQDAgNIADBF AiB7bkMezoRqrjFzmQNzZ2smcrmKJ2ePrfNe3xFyWgQyWwIhAIIMAN2jg39P27mn r0/T4PhfiLBY8naJw7t3bW6siCaU -----END CERTIFICATE----- ` testValidIntermediateCert = ` -----BEGIN CERTIFICATE----- MIIBdTCCARqgAwIBAgICEAAwCgYIKoZIzj0EAwIwIjEgMB4GA1UEAxMXVHJ1c3Rl ZCBJbnRlcm1lZGlhdGUgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zNDA3MjMxODA3 NDlaMCgxJjAkBgNVBAMTHWNsaWVudCBjZXJ0IGZyb20gaW50ZXJtZWRpYXRlMFkw EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErSXc3EmG1iX8r1zra1v+wOFlX3nBTj3N g03l5a5BvAclZ4jRLyU25RAT+LoPZwCag526xCsXQ2rctamWYuftvqM4MDYwEwYD VR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0jBBgwFoAU5bvu/MiiijmpKmc6yIB6Fi0T HP0wCgYIKoZIzj0EAwIDSQAwRgIhAOmeRu2zAnCYHUhdBlH1/+K6pHysCJJHboSO 781NdfTkAiEA/ABjaj2sNNUFDCl/Ayqu7ufv+o5/F6mx3tMdnWZ0rlQ= -----END CERTIFICATE----- ` testValidCertWithDNSSANs = ` -----BEGIN CERTIFICATE----- MIIBljCCATugAwIBAgICEAQwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCAzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE n3orlbZpOzCKyYbyFwUWO/hYcI+uldw2Fczdas+9g02E5mtnZpprxFnnzMipF6X9 PIOVTWEPJ7LRNIN2Bt8zLKNxMG8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j BBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwNwYDVR0RBDAwLoIVYS5jbGllbnQz LmV4YW1wbGUuY29tghViLmNsaWVudDMuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwID SQAwRgIhAI36+egFjZ3NtLlT6xcSpTJtd6dOw/uBXxXnIiYfgoV6AiEA8Eazupyl xiUuFh7lEsr293NN0G7IW6tJW36bWTxYOf8= -----END CERTIFICATE----- ` testValidCertWithEmailSAN = ` -----BEGIN CERTIFICATE----- MIIBfDCCASKgAwIBAgICEAUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE IIdyrCKf7142FXGd1ZU+eKkUu73heahK7iWOpCC3gMks12KKnpkcrt5ezQ1LM51P CI/yWYdmLPHgmzychJIiF6NYMFYwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j BBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwHgYDVR0RBBcwFYETY2xpZW50NEBl eGFtcGxlLmNvbTAKBggqhkjOPQQDAgNIADBFAiBeMogtQTVB9vi+snua/DfcKu18 puJx/UGqXfwpYMObiwIhANwPL6FgGC208+LVca6r09BCh7QsoJCwCdAMTd5V7Pup -----END CERTIFICATE----- ` testValidCertWithIPSAN = ` -----BEGIN CERTIFICATE----- MIIBbTCCAROgAwIBAgICEAYwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE 9JayRtfR5I0U6pk/FMf+LwdAY1+toL0CBwgj6UOgRPQKj2osSwJvGSQg3OBMyoMW 48UoykPMuEx9RgJAdG2rLaNJMEcwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j BBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwDwYDVR0RBAgwBocEwKgKCjAKBggq hkjOPQQDAgNIADBFAiBxtRMWbiwa0kSftQ7KWymaEQzTNwa4DlanaJFitHX7wwIh ALbkwkvT/egNexdQIL8SpP0owUiXvuB/OkmY49XQigub -----END CERTIFICATE----- ` testValidCertWithURISAN = ` -----BEGIN CERTIFICATE----- MIIBhTCCASugAwIBAgICEAcwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE vpHXNca1VPMtp5x3cAgAUjfGEkrJ3oaQmRvbPnbOuxP91GSh1qFDOcnQlLQSI9WV HbaWsCYEQ/cEvCL45/PDqqNhMF8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j BBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwJwYDVR0RBCAwHoYcc3BpZmZlOi8v ZXhhbXBsZS5jb20vZm9vL2JhcjAKBggqhkjOPQQDAgNIADBFAiEA0uz8g6/A+9p9 AnNH20Sk6UiXwuKvy1pxk7TeMd02o6ACIBMsLVs+WIWZLht52+FEUJbpNXSF+IY+ e4rx7Ac0KxRL -----END CERTIFICATE----- ` testValidCertWithUPNSAN = ` -----BEGIN CERTIFICATE----- MIIBiDCCAS2gAwIBAgICEAcwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIzMTgwNzQ5WjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE QfxgDFsW27yUCmT990CwIVQexBdQ+dYRWuOHCnC+dGYESUcU6913hIEZEOwuqvwm eKCX+NOZ+kXiGSxqD+F7C6NjMGEwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j BBgwFoAU4hTNmxqsRHUZ8Nk/m03RHo6HGyAwKQYDVR0RAQH/BB8wHaAbBgorBgEE AYI3FAIDoA0MC3Rlc3RfZGV2aWNlMAoGCCqGSM49BAMCA0kAMEYCIQDw8sM7Eu5F hTgx7XMlMsIlUTs0/n1WrMlr5RcdPlG9tgIhAK9o4Dtlr2bqW7w9RSidNLT6loCJ dgwikvJkMOfcuexx -----END CERTIFICATE----- ` ) func Test_isValidClientCertificate(t *testing.T) { t.Parallel() var noConstraints ClientCertConstraints t.Run("no ca", func(t *testing.T) { valid, err := isValidClientCertificate( "", "", ClientCertificateInfo{Leaf: "WHATEVER!"}, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("no cert", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{}, noConstraints) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("valid cert", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCert, }, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("valid cert with intermediate", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidIntermediateCert, Intermediates: testIntermediateCA, }, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("valid cert missing intermediate", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidIntermediateCert, Intermediates: "", }, noConstraints) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("intermediate CA as root", func(t *testing.T) { valid, err := isValidClientCertificate(testIntermediateCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidIntermediateCert, }, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("unsigned cert", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testUntrustedCert, }, noConstraints) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("not a cert", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: "WHATEVER!", }, noConstraints) assert.Error(t, err, "should return an error") assert.False(t, valid, "should return false") }) t.Run("revoked cert", func(t *testing.T) { revokedCertInfo := ClientCertificateInfo{ Presented: true, Leaf: testRevokedCert, } // The "revoked cert" should otherwise be valid (when no CRL is specified). valid, err := isValidClientCertificate(testCA, "", revokedCertInfo, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, testCRL, revokedCertInfo, noConstraints) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") // Specifying a CRL containing the revoked cert should not affect other certs. valid, err = isValidClientCertificate(testCA, testCRL, ClientCertificateInfo{ Presented: true, Leaf: testValidCert, }, noConstraints) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("chain too deep", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidIntermediateCert, Intermediates: testIntermediateCA, }, ClientCertConstraints{MaxVerifyDepth: 1}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("any SAN", func(t *testing.T) { matchAnySAN := ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile("^.*$"), config.SANTypeEmail: regexp.MustCompile("^.*$"), config.SANTypeIPAddress: regexp.MustCompile("^.*$"), config.SANTypeURI: regexp.MustCompile("^.*$"), }} valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCert, // no SANs }, matchAnySAN) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithDNSSANs, }, matchAnySAN) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithEmailSAN, }, matchAnySAN) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithIPSAN, }, matchAnySAN) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithURISAN, }, matchAnySAN) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") }) t.Run("DNS SAN", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithDNSSANs, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^a\..*\.example\.com$`), }}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithDNSSANs, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeEmail: regexp.MustCompile(`^a\..*\.example\.com$`), // mismatched type }}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("email SAN", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithEmailSAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeEmail: regexp.MustCompile(`^.*@example\.com$`), }}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithEmailSAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeIPAddress: regexp.MustCompile(`^.*@example\.com$`), // mismatched type }}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("IP address SAN", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithIPSAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeIPAddress: regexp.MustCompile(`^192\.168\.10\..*$`), }}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithIPSAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeURI: regexp.MustCompile(`^192\.168\.10\..*$`), // mismatched type }}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("URI SAN", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithURISAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeURI: regexp.MustCompile(`^spiffe://example\.com/.*$`), }}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithURISAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^spiffe://example\.com/.*$`), // mismatched type }}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) t.Run("UserPrincipalName SAN", func(t *testing.T) { valid, err := isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithUPNSAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeUserPrincipalName: regexp.MustCompile(`^test_device$`), }}) assert.NoError(t, err, "should not return an error") assert.True(t, valid, "should return true") valid, err = isValidClientCertificate(testCA, "", ClientCertificateInfo{ Presented: true, Leaf: testValidCertWithURISAN, }, ClientCertConstraints{SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^test-device$`), // mismatched type }}) assert.NoError(t, err, "should not return an error") assert.False(t, valid, "should return false") }) } func TestClientCertConstraintsFromConfig(t *testing.T) { t.Parallel() t.Run("default constraints", func(t *testing.T) { var s config.DownstreamMTLSSettings c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{MaxVerifyDepth: 1}, c) }) t.Run("no constraints", func(t *testing.T) { s := config.DownstreamMTLSSettings{ MaxVerifyDepth: new(uint32), } c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{}, c) }) t.Run("larger max depth", func(t *testing.T) { depth := uint32(5) s := config.DownstreamMTLSSettings{ MaxVerifyDepth: &depth, } c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{MaxVerifyDepth: 5}, c) }) t.Run("one SAN match", func(t *testing.T) { s := config.DownstreamMTLSSettings{ MatchSubjectAltNames: []config.SANMatcher{ {Type: config.SANTypeDNS, Pattern: `.*\.corp\.example\.com`}, }, } c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{ MaxVerifyDepth: 1, SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^(.*\.corp\.example\.com)$`), }, }, c) }) t.Run("multiple SAN matches", func(t *testing.T) { s := config.DownstreamMTLSSettings{ MatchSubjectAltNames: []config.SANMatcher{ {Type: config.SANTypeDNS, Pattern: `.*\.foo\.example\.com`}, {Type: config.SANTypeDNS, Pattern: `.*\.bar\.example\.com`}, }, } c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{ MaxVerifyDepth: 1, SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^(.*\.foo\.example\.com)|(.*\.bar\.example\.com)$`), }, }, c) }) t.Run("multiple SAN matches", func(t *testing.T) { s := config.DownstreamMTLSSettings{ MatchSubjectAltNames: []config.SANMatcher{ {Type: config.SANTypeDNS, Pattern: `.*\.foo\.example\.com`}, {Type: config.SANTypeEmail, Pattern: `.*@example\.com`}, }, } c, err := ClientCertConstraintsFromConfig(&s) assert.NoError(t, err) assert.Equal(t, &ClientCertConstraints{ MaxVerifyDepth: 1, SANMatchers: SANMatchers{ config.SANTypeDNS: regexp.MustCompile(`^(.*\.foo\.example\.com)$`), config.SANTypeEmail: regexp.MustCompile(`^(.*@example\.com)$`), }, }, c) }) t.Run("bad SAN match expression", func(t *testing.T) { s := config.DownstreamMTLSSettings{ MatchSubjectAltNames: []config.SANMatcher{ {Type: config.SANTypeDNS, Pattern: "["}, }, } _, err := ClientCertConstraintsFromConfig(&s) require.Error(t, err) }) } func TestGetUserPrincipalNamesFromSAN(t *testing.T) { type OtherName[T any] struct { TypeID asn1.ObjectIdentifier Value T `asn1:"tag:0"` } type PrintableString struct { Value string } type IA5String struct { Value string `asn1:"ia5"` } type UTF8String struct { Value string `asn1:"utf8"` } upn := func(name string) OtherName[UTF8String] { return OtherName[UTF8String]{ TypeID: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, Value: UTF8String{name}, } } t.Run("OneUserPrincipalName", func(t *testing.T) { type SAN struct { UPN OtherName[UTF8String] `asn1:"tag:0"` } san, err := asn1.Marshal(SAN{upn("hello")}) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.NoError(t, err) assert.Equal(t, []string{"hello"}, names) }) t.Run("MultipleUserPrincipalNames", func(t *testing.T) { type SAN struct { UPN1 OtherName[UTF8String] `asn1:"tag:0"` UPN2 OtherName[UTF8String] `asn1:"tag:0"` UPN3 OtherName[UTF8String] `asn1:"tag:0"` } san, err := asn1.Marshal(SAN{upn("foo"), upn("bar"), upn("baz")}) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.NoError(t, err) assert.Equal(t, []string{"foo", "bar", "baz"}, names) }) t.Run("NoUserPrincipalName", func(t *testing.T) { type SAN struct { DNS string `asn1:"tag:2,ia5"` IP []byte `asn1:"tag:7"` Email string `asn1:"tag:1,ia5"` } san, err := asn1.Marshal(SAN{ "localhost", []byte{127, 0, 0, 1}, "me@example.com", }) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.NoError(t, err) assert.Empty(t, names) }) t.Run("MultipleNameTypes", func(t *testing.T) { type SAN struct { DNS1 string `asn1:"tag:2,ia5"` DNS2 string `asn1:"tag:2,ia5"` Other1 OtherName[IA5String] `asn1:"tag:0"` Other2 OtherName[UTF8String] `asn1:"tag:0"` Other3 OtherName[PrintableString] `asn1:"tag:0"` } san, err := asn1.Marshal(SAN{ "example.com", "example.org", OtherName[IA5String]{ TypeID: asn1.ObjectIdentifier{1, 1, 1, 1}, Value: IA5String{"IA5String"}, }, upn("UserPrincipalName"), OtherName[PrintableString]{ TypeID: asn1.ObjectIdentifier{1, 1, 1, 2}, Value: PrintableString{"PrintableString"}, }, }) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.NoError(t, err) assert.Equal(t, []string{"UserPrincipalName"}, names) }) t.Run("UserPrincipalNameWrongValueType", func(t *testing.T) { type SAN struct { UPN OtherName[PrintableString] `asn1:"tag:0"` } san, err := asn1.Marshal(SAN{ OtherName[PrintableString]{ TypeID: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, Value: PrintableString{"PrintableString"}, }, }) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.ErrorContains(t, err, "expected UTF8String") assert.Empty(t, names) }) t.Run("EmptySAN", func(t *testing.T) { names, err := getUserPrincipalNamesFromSAN(nil) assert.ErrorContains(t, err, "error reading GeneralNames sequence") assert.Empty(t, names) }) t.Run("TruncatedGeneralName", func(t *testing.T) { san := []byte{0x30, 0x02, 0x82, 0x05 /* 5 more bytes expected */} names, err := getUserPrincipalNamesFromSAN(san) assert.ErrorContains(t, err, "error reading GeneralName") assert.Empty(t, names) }) t.Run("OtherNameWrongTypeIDType", func(t *testing.T) { san := []byte{0x30, 0x06, 0xa0, 0x04, 0x02 /* type Integer, not OID */, 0x02, 0x46, 0x01} names, err := getUserPrincipalNamesFromSAN(san) assert.ErrorContains(t, err, "error reading OtherName type ID") assert.Empty(t, names) }) t.Run("UserPrincipalNameWrongValueTag", func(t *testing.T) { type BadOtherName struct { TypeID asn1.ObjectIdentifier Value UTF8String `asn1:"tag:1"` // instead of tag 0 } type SAN struct { UPN BadOtherName `asn1:"tag:0"` } san, err := asn1.Marshal(SAN{ UPN: BadOtherName{ TypeID: oidUserPrincipalName, Value: UTF8String{"hello"}, }, }) require.NoError(t, err) names, err := getUserPrincipalNamesFromSAN(san) assert.ErrorContains(t, err, "error reading UserPrincipalName value") assert.Empty(t, names) }) }