From d495a1531eb51da2c693677d2f1b5192b2f7cc0b Mon Sep 17 00:00:00 2001 From: Kenneth Jenkins <51246568+kenjenkins@users.noreply.github.com> Date: Tue, 23 Jul 2024 14:16:09 -0700 Subject: [PATCH] authorize: add UserPrincipalName SAN match Add support for UserPrincipalName SAN matching to the policy evaluator. Add unit tests. Unfortunately, adding a new test cert requires regenerating the existing ones as well, because the CA key isn't saved. --- authorize/evaluator/functions.go | 69 +++++ authorize/evaluator/functions_test.go | 239 ++++++++++++------ authorize/evaluator/gen-test-certs.go | 42 +++ authorize/evaluator/headers_evaluator_test.go | 2 +- 4 files changed, 279 insertions(+), 73 deletions(-) diff --git a/authorize/evaluator/functions.go b/authorize/evaluator/functions.go index 10ab2f863..73cdcd1c2 100644 --- a/authorize/evaluator/functions.go +++ b/authorize/evaluator/functions.go @@ -3,6 +3,7 @@ package evaluator import ( "context" "crypto/x509" + "encoding/asn1" "encoding/json" "encoding/pem" "errors" @@ -11,6 +12,8 @@ import ( "strings" lru "github.com/hashicorp/golang-lru/v2" + "golang.org/x/crypto/cryptobyte" + cb_asn1 "golang.org/x/crypto/cryptobyte/asn1" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/log" @@ -242,6 +245,17 @@ func validateClientCertificateSANs(chain []*x509.Certificate, matchers SANMatche } } } + if r := matchers[config.SANTypeUserPrincipalName]; r != nil { + names, err := getUserPrincipalNamesFromCert(cert) + if err != nil { + return err + } + for _, name := range names { + if r.MatchString(name) { + return nil + } + } + } return errNoSANMatch } @@ -256,3 +270,58 @@ func parseCertificate(pemStr string) (*x509.Certificate, error) { } return x509.ParseCertificate(block.Bytes) } + +var ( + oidSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17} + oidUserPrincipalName = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 311, 20, 2, 3} + otherNameTag = cb_asn1.Tag(0).Constructed().ContextSpecific() + upnValueTag = cb_asn1.Tag(0).Constructed().ContextSpecific() +) + +func getUserPrincipalNamesFromSAN(raw []byte) ([]string, error) { + san := cryptobyte.String(raw) + var generalNames cryptobyte.String + if !san.ReadASN1(&generalNames, cb_asn1.SEQUENCE) { + return nil, errors.New("error reading GeneralNames sequence") + } + var upns []string + for !generalNames.Empty() { + var name cryptobyte.String + var tag cb_asn1.Tag + if !generalNames.ReadAnyASN1(&name, &tag) { + return nil, errors.New("error reading GeneralName") + } else if tag != otherNameTag { + continue + } + + var oid asn1.ObjectIdentifier + if !name.ReadASN1ObjectIdentifier(&oid) { + return nil, errors.New("error reading OtherName type ID") + } else if !oid.Equal(oidUserPrincipalName) { + continue + } + + var value cryptobyte.String + if !name.ReadAnyASN1(&value, &tag) { + return nil, errors.New("error reading UserPrincipalName value") + } else if tag != upnValueTag { + return nil, fmt.Errorf("unexpected UserPrincipalName data tag 0x%x", tag) + } + + var utf8string cryptobyte.String + if !value.ReadASN1(&utf8string, cb_asn1.UTF8String) { + return nil, errors.New("error reading UserPrincipalName: expected UTF8String") + } + upns = append(upns, string(utf8string)) + } + return upns, nil +} + +func getUserPrincipalNamesFromCert(cert *x509.Certificate) ([]string, error) { + for _, ext := range cert.Extensions { + if ext.Id.Equal(oidSubjectAltName) { + return getUserPrincipalNamesFromSAN(ext.Value) + } + } + return nil, nil +} diff --git a/authorize/evaluator/functions_test.go b/authorize/evaluator/functions_test.go index 05517c91b..63d97ac8b 100644 --- a/authorize/evaluator/functions_test.go +++ b/authorize/evaluator/functions_test.go @@ -1,6 +1,7 @@ package evaluator import ( + "encoding/base64" "regexp" "testing" @@ -18,134 +19,148 @@ import ( const ( testCA = ` -----BEGIN CERTIFICATE----- -MIIBaDCCAQ6gAwIBAgICEAAwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAaMRgw +MIIBZzCCAQ6gAwIBAgICEAAwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAaMRgw FgYDVQQDEw9UcnVzdGVkIFJvb3QgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC -AAR2/RkzmSK6paoeTKFx1Bd52ZCg29ulJlMxFdSZT8FlmmaK9mN6KWwO+NHYObiW -y3AQuoSTrZXlrlRW5ANvMI+io0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUJGFVU2UOvOVgaY9YcCUiunGpiCQwCgYIKoZIzj0E -AwIDSAAwRQIhAMU5/NjpitOSbUobtjeOriPH8JRo9qy1iFyeVNAcdVvgAiAewq2A -PhgzWTw5F9PJg++9i+xGQTqHs3ZirG27cCjvhQ== +AATMLdnk1HSlNQCNimWztrBbBU0xghli5LWSk7P1/1CW3JPvujT0xC2tEpooAxWi +XECL22GH9E2pH/dQoDj2FP41o0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUoLhiwb9DEY80NUm/XfiLszTnetcwCgYIKoZIzj0E +AwIDRwAwRAIgWWAReqEjmvzpUKupOlcmEA4WBoPpsWWTDzFFApWE/+0CIG6FEkwQ +rJ4htyRqNMapRJmIHqgVxhyV/Pb7t4tsp6d7 -----END CERTIFICATE----- ` testValidCert = ` -----BEGIN CERTIFICATE----- MIIBYjCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAeMRww +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAeMRww GgYDVQQDExN0cnVzdGVkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D -AQcDQgAEcWa1Bz6mpsLnM1VD8gtzELjzjEp9Dopp/xWScFO9qtay5SBOeX+Ftr0O -8+/RkoKHzGgZ80gr6xQyUJL3MCwVZKM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw -HwYDVR0jBBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwCgYIKoZIzj0EAwIDSAAw -RQIgXM1ogmy0vcz4lYzji5X3In1n2GLOFNTgucFPkM0GtqgCIQCsXPs/0OjSFyDR -FBqAm1NqDJcxq685fS9t3VfHwapcVA== +AQcDQgAEUk42ibrv64GOkeoZdw+AUiohrtmg2g89jq60E7+HPBrDHWI+jZ22EtBe +N9pphc6hktrlGM3oHn95NOemJ/mlAaM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw +HwYDVR0jBBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwCgYIKoZIzj0EAwIDSAAw +RQIgGVB3Q4gFtZNgzjUUe1TuhJLy4NY1vRMrtVk1ywEC4hoCIQCg7XSIirpS4Mxc +kB9Ugn4FUX8SDD5+md2IReM27uKBjw== -----END CERTIFICATE----- ` testUntrustedCert = ` -----BEGIN CERTIFICATE----- -MIIBZjCCAQygAwIBAgICEAEwCgYIKoZIzj0EAwIwHDEaMBgGA1UEAxMRVW50cnVz -dGVkIFJvb3QgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zMzA4MDcxODAzMjFaMCAx +MIIBZTCCAQygAwIBAgICEAEwCgYIKoZIzj0EAwIwHDEaMBgGA1UEAxMRVW50cnVz +dGVkIFJvb3QgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zNDA3MjEyMTA5MDFaMCAx HjAcBgNVBAMTFXVudHJ1c3RlZCBjbGllbnQgY2VydDBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABJxEIKqLhhMEm5XZXkT+p+hlC2TFyaW0HIZqoE9navJrAcUB8L2M -mVQ+/wLaCznJHLeSLn46uGH5p1hoGFqOrdajODA2MBMGA1UdJQQMMAoGCCsGAQUF -BwMCMB8GA1UdIwQYMBaAFIp2rlIiSnr33ea3cGyLsX4LEYwWMAoGCCqGSM49BAMC -A0gAMEUCIDtJIZJDcqIYaDXhZFs0nd0nHER8IGP9n4BBFMWewAb2AiEAlQyavOxw -iTQQxt0rXB4Ox5zWpU9q68+F9BGBkQKTsBs= +SM49AwEHA0IABMD1+g0Dx5xcezQWJsWJX9AoNlPLsaqspsVS/rJoMoa01gE3fh8y +KaVH3N/SQH4E54TUywJA06wSIGN3SsNgpnajODA2MBMGA1UdJQQMMAoGCCsGAQUF +BwMCMB8GA1UdIwQYMBaAFHqcIcMoPGoBDLYSIol5EqKgXAA+MAoGCCqGSM49BAMC +A0cAMEQCIDCSM5mZkwApd+Ne0fYK3o0dP9CKeEd31lzF9PlMAzU2AiBL8EBVN5T6 +M74llANbsQV6/9O3WSDYsvxvAGK/aN2pBg== -----END CERTIFICATE----- ` testRevokedCert = ` -----BEGIN CERTIFICATE----- MIIBYjCCAQigAwIBAgICEAIwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAeMRww +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAeMRww GgYDVQQDExNyZXZva2VkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D -AQcDQgAEcnoO4EM72C7xL31RE9e6m9YJYyF6E4JloASECd8mdiXPlMXIjq8MZHB5 -28mFAVQNE7erAtBftID1SbuY4IpXxqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw -HwYDVR0jBBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwCgYIKoZIzj0EAwIDSAAw -RQIgUUETSO064YIu+VKnyRb0yBnNTjXLy3TvGuYgZI8VX0YCIQDd0gyNEC5YLvRN -njxfnLoimp+TzTVzvsCokUbNSNRKJA== +AQcDQgAEfOs33d2sg3TQg19dh1bA5gv+wUljqfkykl/rj5vh3PwMoNgf38M09Lbs +fT6kz7wpKEA0G9/h/f2c4y59/d34ZqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw +HwYDVR0jBBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwCgYIKoZIzj0EAwIDSAAw +RQIhAKNJwVxMvKIjkMfk9JhePG24CGawlIzCMJLRExQnIxzoAiAbMOnxje3tr/Mg +kl9linML1Km4f5aqOp2+TF9O/EHhKg== -----END CERTIFICATE----- ` testCRL = ` -----BEGIN X509 CRL----- -MIHeMIGFAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD1RydXN0ZWQgUm9vdCBD -QRgPMDAwMTAxMDEwMDAwMDBaMBUwEwICEAIXDTIzMDgxMDE4MDMyMVqgMDAuMB8G -A1UdIwQYMBaAFCRhVVNlDrzlYGmPWHAlIrpxqYgkMAsGA1UdFAQEAgIgADAKBggq -hkjOPQQDAgNIADBFAiEAumtTtjiQt1VsbsEnyr+xbpK0KmzKvkpxIVgE1M9CND0C -IA8zx5clcaGIT5xRnBLZW7RwA37IOmB+7zjAuJQpmKKp +MIHdMIGFAgEBMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD1RydXN0ZWQgUm9vdCBD +QRgPMDAwMTAxMDEwMDAwMDBaMBUwEwICEAIXDTI0MDcyMzIxMDkwMVqgMDAuMB8G +A1UdIwQYMBaAFKC4YsG/QxGPNDVJv134i7M053rXMAsGA1UdFAQEAgIgADAKBggq +hkjOPQQDAgNHADBEAiA6zjKyqK0IE8bO/LtnQT2CSRolOcdwHPMjtTkoO7ht8gIg +Qo8S83mFqwG14NiwaudxpYyYfGcCJZ6aaxqLc03ixiA= -----END X509 CRL----- ` testIntermediateCA = ` -----BEGIN CERTIFICATE----- MIIBkTCCATegAwIBAgICEAMwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAiMSAw +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAiMSAw HgYDVQQDExdUcnVzdGVkIEludGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqG -SM49AwEHA0IABMY+zxL/2dNORuha3uVVOXZYIkTpa9V8N9UVrM15HOHkrdLlz1qk -4wbePkkoGtNRzoayb0iZqeA4YjOxqyPG8emjYzBhMA4GA1UdDwEB/wQEAwIBBjAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQpGNmcLLM3vHiOADYGPDQL8AhkyDAf -BgNVHSMEGDAWgBQkYVVTZQ685WBpj1hwJSK6camIJDAKBggqhkjOPQQDAgNIADBF -AiEAnR6xrk7OCk91ymtzU+duZXDqDq35w0oO+MM8nqpac4YCIED+6c9dJKvRCc/C -nP8PMxRaUsbQet1woE7Fckn5tK4N +SM49AwEHA0IABNPOBn1f6EJYlOBs9E4ZllV3rVvzGbh7Z3f3klIfKv0nmYZqTYsE +06GbMlc+Rdw//wQ40j9+Gnvk/yTVPUydtSSjYzBhMA4GA1UdDwEB/wQEAwIBBjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQtnNz7iWzU1Fp427ksQsEf31WvgTAf +BgNVHSMEGDAWgBSguGLBv0MRjzQ1Sb9d+IuzNOd61zAKBggqhkjOPQQDAgNIADBF +AiEAzOI48p3sxhFnpp2s0LFxjzQXwOUqsKtlkWh16hEzG4MCIGspIDiBlxE594+M +iiciSIzesgppOJb1Hk/JS9r6qYhz -----END CERTIFICATE----- ` testValidIntermediateCert = ` -----BEGIN CERTIFICATE----- MIIBdTCCARqgAwIBAgICEAAwCgYIKoZIzj0EAwIwIjEgMB4GA1UEAxMXVHJ1c3Rl -ZCBJbnRlcm1lZGlhdGUgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zMzA4MDcxODAz -MjFaMCgxJjAkBgNVBAMTHWNsaWVudCBjZXJ0IGZyb20gaW50ZXJtZWRpYXRlMFkw -EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE5SVyYjNRuuFXGjEmCcuVtMq7e2bmndPK -bRJ7lJ5cc0kZSoNJes5wXOtGRFbx3+admRHq+w1XEBXOe+yRUB8kdKM4MDYwEwYD -VR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0jBBgwFoAUKRjZnCyzN7x4jgA2Bjw0C/AI -ZMgwCgYIKoZIzj0EAwIDSQAwRgIhAMj0O2wDRLoxGIPUDOmUfYxmxglOecQhSkWO -NBtItSxmAiEAy0XCzvpL6XOZU3zxyCjTdJQa2RiC6YnypMaCaETzCaU= +ZCBJbnRlcm1lZGlhdGUgQ0EwIBgPMDAwMTAxMDEwMDAwMDBaFw0zNDA3MjEyMTA5 +MDFaMCgxJjAkBgNVBAMTHWNsaWVudCBjZXJ0IGZyb20gaW50ZXJtZWRpYXRlMFkw +EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi9mkxtKlhiiCHteqDHZP+YH4awImLtEx +i7s/Nt1AWp3VMQ/cxRsZcmRHceGIpPoinYpmGfVppxy9fddZ4IBjw6M4MDYwEwYD +VR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0jBBgwFoAULZzc+4ls1NRaeNu5LELBH99V +r4EwCgYIKoZIzj0EAwIDSQAwRgIhAJGUfGvzsTwMMqLzhSNdmKUWyajbN+wRF2gr +3CzI77OZAiEAh40Bew0mQQUs3WWilY0BauxtDtdcjX/IerxKTzEXKbg= -----END CERTIFICATE----- ` testValidCertWithDNSSANs = ` -----BEGIN CERTIFICATE----- -MIIBlTCCATugAwIBAgICEAQwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAYMRYw +MIIBlDCCATugAwIBAgICEAQwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCAzMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -LFai39b7TYauNjg4M58f0qY6jTC7xEOhE84wTTcevZvH/t2Y7U0BBNGkvpb14yxh -60vrRKZA9t9G6ZvWKcY/BKNxMG8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j -BBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwNwYDVR0RBDAwLoIVYS5jbGllbnQz +Dw9uoVvSDyVpqRN79Dtd3pf8pbTTKV8ZFDMa+pft3cWCSbzsxkHjx5yfiYmy94m0 +IAUEkpsivA7w5zqhqZfONaNxMG8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j +BBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwNwYDVR0RBDAwLoIVYS5jbGllbnQz LmV4YW1wbGUuY29tghViLmNsaWVudDMuZXhhbXBsZS5jb20wCgYIKoZIzj0EAwID -SAAwRQIgBSw8MsKWPPcpGtuVpNJonTEthIOjIGXswxiYG49y2BECIQC5D1DCX/lY -KSwF4aapPx4906VujTL+Ehj8L5ImUYcPbA== +RwAwRAIgTgkgYXb/6sErId2S/1X8TkdQETmMeQ85e0zQLC2WGn0CIAPF5nrlf5EL +VLK0wso+qCFmqrPNnv4X8nYOEc3L7kPA -----END CERTIFICATE----- ` testValidCertWithEmailSAN = ` -----BEGIN CERTIFICATE----- -MIIBezCCASKgAwIBAgICEAUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAYMRYw +MIIBfTCCASKgAwIBAgICEAUwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -WvX8BnCrzUSpLrYka8ed+bz6/HoXUvq5nRqysKe0nGYSsXKRjxLdCG8AKsoGIQIv -KOQScf/4TJUNIUY4XOsFI6NYMFYwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j -BBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwHgYDVR0RBBcwFYETY2xpZW50NEBl -eGFtcGxlLmNvbTAKBggqhkjOPQQDAgNHADBEAiAMYGTjUBqgnai8UL3B/iQkCkMb -xgCC1ZYdZaJ1RBwFfgIgIhjQZ2s6dTaah/LzYJ9ZwMvSA86XQvzTVSuT6s+RJw0= +zMPGmaQ/Hhss/Pil1ku5QgFmVkaZdMHllql508HddNQ96t24vddILPS1YZwRv1SQ +D47sdwRcrgLcYvXAYhnfxKNYMFYwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j +BBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwHgYDVR0RBBcwFYETY2xpZW50NEBl +eGFtcGxlLmNvbTAKBggqhkjOPQQDAgNJADBGAiEAiKp/7Cv8tUJBUr+Z+T6ztghs +hhSH/LZcLzRHJ8jtACoCIQD78Ut4V6/8eSOS3DHIDwqXwOZ/L06DwugzYP5/jlZA +cQ== -----END CERTIFICATE----- ` testValidCertWithIPSAN = ` -----BEGIN CERTIFICATE----- MIIBbTCCAROgAwIBAgICEAYwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAYMRYw +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA1MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -6+9BE+aomrR2Mdnx4iFY63t0hsVD6rYaHBW1b9roFQX6Cor4YeUfkEEF4LrGeAyb -wcqb6G1ExgNyjEh10Ai1M6NJMEcwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j -BBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwDwYDVR0RBAgwBocEwKgKCjAKBggq -hkjOPQQDAgNIADBFAiEA/mq32YZZAacOH/P/wjvfD1n74DD/GkhW4kfS72Z0oGQC -IAQ+L8E78JOLaPWXiL7WFpVrb0hOHkV2m9Qw4GB41mUN +cRD582BfNyLzyuvtxR/Z19mwWqRF6Kxx5ZSovtPKIeJ75Okiv1q20TOwGrEYLmHr +YCBrMd3YM56KKcEhSUi/nqNJMEcwEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j +BBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwDwYDVR0RBAgwBocEwKgKCjAKBggq +hkjOPQQDAgNIADBFAiAv+MGYGG9SaQFekl1e7zIG0rxMkbS8UPmkklR/RuXQ+gIh +ALvBj9aAXW9wN8nYLH9L2bPbMT//a42VuD6qvi6B0i6P -----END CERTIFICATE----- ` testValidCertWithURISAN = ` -----BEGIN CERTIFICATE----- MIIBhTCCASugAwIBAgICEAcwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl -ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwODA3MTgwMzIxWjAYMRYw +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAYMRYw FAYDVQQDEw1jbGllbnQgY2VydCA2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE -7mHladapSY5PlVYToL1dHr0tJPGe5XVP8DSZJz+WWyS9tWsuEsK6P5yeZrbWASOX -foH7iVIdx3DMyukGsvMX+KNhMF8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j -BBgwFoAUJGFVU2UOvOVgaY9YcCUiunGpiCQwJwYDVR0RBCAwHoYcc3BpZmZlOi8v -ZXhhbXBsZS5jb20vZm9vL2JhcjAKBggqhkjOPQQDAgNIADBFAiAhzElKeGJzp2zP -GOTUEy0f6b2tvMYGDLQxCcp4bc4QuQIhAPwX4Y3Cr7uazQlbwL6D51y9NCcDyj3D -Z18vZNxm9ZR1 +B7Znrqifp50mCqJfdoCvEu0UP+W1qUZm1dckfMq02f9kgbn2JGgvb1nGE8gKA0n1 +V6/QtuIUBnyosfX3IuF9qqNhMF8wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j +BBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwJwYDVR0RBCAwHoYcc3BpZmZlOi8v +ZXhhbXBsZS5jb20vZm9vL2JhcjAKBggqhkjOPQQDAgNIADBFAiAwp1maaT976oWE +f8RnxAPGX1Ecc77A6uhOM0ZCVrYDCQIhAJf5MW7BddAtbRYHyUEhwaVFA9Ci56m9 +TEgriIGNirO9 +-----END CERTIFICATE----- +` + testValidCertWithUPNSAN = ` +-----BEGIN CERTIFICATE----- +MIIBhDCCASqgAwIBAgICEAcwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl +ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzQwNzIxMjEwOTAxWjAYMRYw +FAYDVQQDEw1jbGllbnQgY2VydCA3MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +Pbsk3ZsS7dPwVh+G+hG/nv9PcHfFpOrCiAP0/orxhTRiYD2QbxCB9KBC2YZKrMGF +pvhqM/fX6r1hB6o1Gz/J3KNgMF4wEwYDVR0lBAwwCgYIKwYBBQUHAwIwHwYDVR0j +BBgwFoAUoLhiwb9DEY80NUm/XfiLszTnetcwJgYDVR0RBB8wHaAbBgorBgEEAYI3 +FAIDoA0MC3Rlc3RfZGV2aWNlMAoGCCqGSM49BAMCA0gAMEUCIQDG82BiX0pzkvW+ +UgGq54X7ItNLu2uBFBTgR8NCJpBCMAIgaBzSkAyTSbbLU/gRrhD6HiyGoxDn38xh +HcsXWWvNgTQ= -----END CERTIFICATE----- ` ) @@ -366,6 +381,25 @@ func Test_isValidClientCertificate(t *testing.T) { 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) { @@ -452,3 +486,64 @@ func TestClientCertConstraintsFromConfig(t *testing.T) { require.Error(t, err) }) } + +func TestGetUserPrincipalNamesFromSAN(t *testing.T) { + b64 := func(raw string) []byte { + b, err := base64.StdEncoding.DecodeString(raw) + require.NoError(t, err) + return b + } + + // One UserPrincipalName. + names, err := getUserPrincipalNamesFromSAN(b64(` +MBegFQYKKwYBBAGCNxQCA6AHDAVoZWxsbw==`)) + assert.NoError(t, err) + assert.Equal(t, []string{"hello"}, names) + + // Multiple UserPrincipalNames. + names, err = getUserPrincipalNamesFromSAN(b64(` +MD+gEwYKKwYBBAGCNxQCA6AFDANmb2+gEwYKKwYBBAGCNxQCA6AFDANiYXKgEwYKKwYBBAGCNxQCA6AF +DANiYXo=`)) + assert.NoError(t, err) + assert.Equal(t, []string{"foo", "bar", "baz"}, names) + + // Several OtherNames, only one UserPrincipalName. + names, err = getUserPrincipalNamesFromSAN(b64(` +MFigFgYDKQEBoA8bDUdlbmVyYWxTdHJpbmegIQYKKwYBBAGCNxQCA6ATDBFVc2VyUHJpbmNpcGFsTmFt +ZaAbBgMpAQKgFB4SAEIATQBQAFMAdAByAGkAbgBn`)) + assert.NoError(t, err) + assert.Equal(t, []string{"UserPrincipalName"}, names) + + // Two DNS names, no OtherNames. + names, err = getUserPrincipalNamesFromSAN(b64(` +MC6CFWEuY2xpZW50My5leGFtcGxlLmNvbYIVYi5jbGllbnQzLmV4YW1wbGUuY29t`)) + assert.NoError(t, err) + assert.Empty(t, names) + + // A UserPrincipalName with the wrong data type (GeneralString). + names, err = getUserPrincipalNamesFromSAN(b64(` +MB+gHQYKKwYBBAGCNxQCA6APGw1HZW5lcmFsU3RyaW5n`)) + assert.ErrorContains(t, err, "expected UTF8String") + assert.Empty(t, names) + + // Other malformed inputs. + names, err = getUserPrincipalNamesFromSAN(nil) + assert.ErrorContains(t, err, "error reading GeneralNames sequence") + assert.Empty(t, names) + + names, err = getUserPrincipalNamesFromSAN(b64("MA+gDwYCRgGgBwwFaGVsbG8=")) + assert.ErrorContains(t, err, "error reading GeneralName") + assert.Empty(t, names) + + names, err = getUserPrincipalNamesFromSAN(b64("MA+gDQICRgGgBwwFaGVsbG8=")) + assert.ErrorContains(t, err, "error reading OtherName type ID") + assert.Empty(t, names) + + names, err = getUserPrincipalNamesFromSAN(b64("MBegFQYKKwYBBAGCNxQCA6AIDAVoZWxsbw==")) + assert.ErrorContains(t, err, "error reading UserPrincipalName value") + assert.Empty(t, names) + + names, err = getUserPrincipalNamesFromSAN(b64("MBegFQYKKwYBBAGCNxQCA6EHDAVoZWxsbw==")) + assert.ErrorContains(t, err, "unexpected UserPrincipalName data tag") + assert.Empty(t, names) +} diff --git a/authorize/evaluator/gen-test-certs.go b/authorize/evaluator/gen-test-certs.go index 0d4f3a7cf..a412691c4 100644 --- a/authorize/evaluator/gen-test-certs.go +++ b/authorize/evaluator/gen-test-certs.go @@ -8,6 +8,7 @@ import ( "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "fmt" "log" @@ -68,6 +69,33 @@ func newCRL( return string(pem.EncodeToMemory(&pem.Block{Type: "X509 CRL", Bytes: der})) } +// Returns a raw SubjectAltName extension with a single UserPrincipalName. +func newSANUserPrincipalName(upnValue string) []byte { + type UPN struct { + Utf8String string `asn1:"utf8"` + } + type OtherName struct { + OID asn1.ObjectIdentifier + Value UPN `asn1:"tag:0"` + } + type GeneralNames struct { + OtherName OtherName `asn1:"tag:0"` + } + san, err := asn1.Marshal(GeneralNames{ + OtherName: OtherName{ + OID: asn1.ObjectIdentifier{ + 1, 3, 6, 1, 4, 1, 311, 20, 2, 3}, + Value: UPN{ + Utf8String: upnValue, + }, + }, + }) + if err != nil { + log.Fatalln(err) + } + return san +} + // Generates new test certificates and CRLs. func main() { notAfter := time.Now().Add(3650 * 24 * time.Hour) @@ -190,6 +218,19 @@ func main() { ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, }, rootCA, rootKey) + trustedClientCert7PEM, _, _ := newCertificate(&x509.Certificate{ + SerialNumber: big.NewInt(0x1007), + Subject: pkix.Name{ + CommonName: "client cert 7", + }, + NotAfter: notAfter, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + ExtraExtensions: []pkix.Extension{{ + Id: asn1.ObjectIdentifier{2, 5, 29, 17}, + Value: newSANUserPrincipalName("test_device"), + }}, + }, rootCA, rootKey) + fmt.Println(` const ( testCA = ` + "`\n" + rootPEM + "`" + ` @@ -203,6 +244,7 @@ const ( testValidCertWithEmailSAN = ` + "`\n" + trustedClientCert4PEM + "`" + ` testValidCertWithIPSAN = ` + "`\n" + trustedClientCert5PEM + "`" + ` testValidCertWithURISAN = ` + "`\n" + trustedClientCert6PEM + "`" + ` + testValidCertWithUPNSAN = ` + "`\n" + trustedClientCert7PEM + "`" + ` ) `) } diff --git a/authorize/evaluator/headers_evaluator_test.go b/authorize/evaluator/headers_evaluator_test.go index 64c8e4940..60855925a 100644 --- a/authorize/evaluator/headers_evaluator_test.go +++ b/authorize/evaluator/headers_evaluator_test.go @@ -171,7 +171,7 @@ func TestHeadersEvaluator(t *testing.T) { assert.Equal(t, "CUSTOM_VALUE", output.Headers.Get("X-Custom-Header")) assert.Equal(t, "ID_TOKEN", output.Headers.Get("X-ID-Token")) assert.Equal(t, "ACCESS_TOKEN", output.Headers.Get("X-Access-Token")) - assert.Equal(t, "ebf421e323e31c3900a7985a16e72c59f45f5a2c15283297567e226b3b17d1a1", + assert.Equal(t, "d850ddd39279f34f01b13548f364dd529b23531aaffe8f592c03bb79f76bf031", output.Headers.Get("Client-Cert-Fingerprint")) assert.Equal(t, "escaped $dollar sign", output.Headers.Get("Foo")) })