mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
Add a new 'user_principal_name' type to the downstream mTLS match_subject_alt_names option. This corresponds to the 'OtherName' type with type-id 1.3.6.1.4.1.311.20.2.3 and a UTF8String value. Add support for UserPrincipalName SAN matching to the policy evaluator.
631 lines
23 KiB
Go
631 lines
23 KiB
Go
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)
|
|
})
|
|
}
|