pomerium/pkg/policy/criteria/client_certificate_test.go
Kenneth Jenkins acbad4fb96
policy: add client cert SAN match criteria (#4913)
Expand the Certificate Matcher to support matching on DNS, email, or URI
Subject Alternative Names, using the existing String Matcher conditions.
2024-01-24 16:05:22 -08:00

299 lines
7.6 KiB
Go

package criteria
import (
"strings"
"testing"
"github.com/open-policy-agent/opa/ast"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pomerium/pomerium/pkg/policy/parser"
)
const testCert = `
-----BEGIN CERTIFICATE-----
MIIBYTCCAQigAwIBAgICEAEwCgYIKoZIzj0EAwIwGjEYMBYGA1UEAxMPVHJ1c3Rl
ZCBSb290IENBMCAYDzAwMDEwMTAxMDAwMDAwWhcNMzMwNzMxMTUzMzE5WjAeMRww
GgYDVQQDExN0cnVzdGVkIGNsaWVudCBjZXJ0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
AQcDQgAEfAYP3ZwiKJgk9zXpR/CMHYlAxjweJaMJihIS2FTA5gb0xBcTEe5AGpNF
CHWPk4YCB25VeHg9GmY9Q1+qDD1hdqM4MDYwEwYDVR0lBAwwCgYIKwYBBQUHAwIw
HwYDVR0jBBgwFoAUXep6D8FTP6+5ZdR/HjP3pYfmxkwwCgYIKoZIzj0EAwIDRwAw
RAIgProROtxpvKS/qjrjonSvacnhdU0JwoXj2DgYvF/qjrUCIAXlHkdEzyXmTLuu
/YxuOibV35vlaIzj21GRj4pYmVR1
-----END CERTIFICATE-----`
// testCertWithSANs is a certificate with 6 Subject Alternative Names:
// DNS:1.example.com, DNS:2.example.com, email:email-1@example.com,
// email:email-2@example.com, URI:https://example.com/uri-1, and
// URI:https://example.com/uri-2.
const testCertWithSANs = `
-----BEGIN CERTIFICATE-----
MIIB9TCCAZugAwIBAgIDAIABMAoGCCqGSM49BAMCMBoxGDAWBgNVBAMTD1RydXN0
ZWQgUm9vdCBDQTAeFw0yNDAxMjIyMzU1NTNaFw0zNDAxMTkyMzU1NTNaMCUxIzAh
BgNVBAMTGmNsaWVudCBjZXJ0IHdpdGggbWFueSBTQU5zMFkwEwYHKoZIzj0CAQYI
KoZIzj0DAQcDQgAEJNVizgh7I/609xD6Dik7QrIzwSp6zIgSeKEfekic7r3rd8fC
0W84UORBjFXxRa4nxj8tyanN4PreD1veACPjHqOBxDCBwTATBgNVHSUEDDAKBggr
BgEFBQcDAjAfBgNVHSMEGDAWgBQ1yn6j/DJdpmqyNIV8/lJBYsIuyzCBiAYDVR0R
BIGAMH6CDTEuZXhhbXBsZS5jb22CDTIuZXhhbXBsZS5jb22BE2VtYWlsLTFAZXhh
bXBsZS5jb22BE2VtYWlsLTJAZXhhbXBsZS5jb22GGWh0dHBzOi8vZXhhbXBsZS5j
b20vdXJpLTGGGWh0dHBzOi8vZXhhbXBsZS5jb20vdXJpLTIwCgYIKoZIzj0EAwID
SAAwRQIgKqRs9N3EOmzW2ZPQgJh2un6XaQbXtyE9O9TZEQGFr2gCIQCC16tr754m
z60udX689FtwwnWYmteZsZstBoEbPSTzWw==
-----END CERTIFICATE-----`
func TestClientCertificate(t *testing.T) {
t.Parallel()
cases := []struct {
label string
policy string
cert string
expected A
}{
{
"no certificate",
`allow:
or:
- client_certificate:
fingerprint: 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704`,
"",
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
},
{
"no fingerprint match",
`allow:
or:
- client_certificate:
fingerprint: df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a`,
testCert,
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
},
{
"fingerprint match",
`allow:
or:
- client_certificate:
fingerprint: 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704`,
testCert,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"fingerprint list match",
`allow:
or:
- client_certificate:
fingerprint:
- 17859273e8a980631d367b2d5a6a6635412b0f22835f69e47b3f65624546a704
- df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a`,
testCert,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"spki hash match",
`allow:
or:
- client_certificate:
spki_hash: FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U=`,
testCert,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"spki hash list match",
`allow:
or:
- client_certificate:
spki_hash:
- FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U=
- NvqYIYSbgK2vCJpQhObf77vv+bQWtc5ek5RIOwPiC9A=`,
testCert,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"no email match",
`allow:
or:
- client_certificate:
san_email:
is: not-present@example.com`,
testCertWithSANs,
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
},
{
"email match",
`allow:
or:
- client_certificate:
san_email:
is: email-1@example.com`,
testCertWithSANs,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"no dns match",
`allow:
or:
- client_certificate:
san_dns:
is: not-present.example.com`,
testCertWithSANs,
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
},
{
"dns match",
`allow:
or:
- client_certificate:
san_dns:
is: 1.example.com`,
testCertWithSANs,
A{true, A{ReasonClientCertificateOK}, M{}},
},
{
"no uri match",
`allow:
or:
- client_certificate:
san_uri:
is: https://example.com/not-present`,
testCertWithSANs,
A{false, A{ReasonClientCertificateUnauthorized}, M{}},
},
{
"uri match",
`allow:
or:
- client_certificate:
san_uri:
is: 'https://example.com/uri-1'`,
testCertWithSANs,
A{true, A{ReasonClientCertificateOK}, M{}},
},
}
for i := range cases {
c := cases[i]
t.Run(c.label, func(t *testing.T) {
t.Parallel()
input := Input{
HTTP: InputHTTP{
ClientCertificate: ClientCertificateInfo{
Leaf: c.cert,
},
},
}
res, err := evaluate(t, c.policy, nil, input)
require.NoError(t, err)
assert.Equal(t, c.expected, res["allow"])
})
}
}
func TestCanonicalCertFingerprint(t *testing.T) {
t.Parallel()
cases := []struct {
label string
input string
output string
err string
}{
{
"object",
`{}`, "", "certificate fingerprint must be a string (was {})",
},
{
"empty",
`""`, "", "certificate fingerprint must not be empty",
},
{
"SHA-1 fingerprint",
`"B1:E6:A2:DC:DD:6B:87:A4:9B:C5:7C:3B:7C:7F:1C:74:9A:DB:88:36"`,
"", "unsupported certificate fingerprint format (B1:E6:A2:DC:DD:6B:87:A4:9B:C5:7C:3B:7C:7F:1C:74:9A:DB:88:36)",
},
{
"uppercase short",
`"DF6FF72FE9116521268F6F2DD4966F51DF479883FE7037B39F75916AC3049D1A"`,
"", "unsupported certificate fingerprint format (DF6FF72FE9116521268F6F2DD4966F51DF479883FE7037B39F75916AC3049D1A)",
},
{
"valid short",
`"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a"`,
"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a", "",
},
{
"lowercase long",
`"df:6f:f7:2f:e9:11:65:21:26:8f:6f:2d:d4:96:6f:51:df:47:98:83:fe:70:37:b3:9f:75:91:6a:c3:04:9d:1a"`,
"", "unsupported certificate fingerprint format (df:6f:f7:2f:e9:11:65:21:26:8f:6f:2d:d4:96:6f:51:df:47:98:83:fe:70:37:b3:9f:75:91:6a:c3:04:9d:1a)",
},
{
"valid long",
`"DF:6F:F7:2F:E9:11:65:21:26:8F:6F:2D:D4:96:6F:51:DF:47:98:83:FE:70:37:B3:9F:75:91:6A:C3:04:9D:1A"`,
"df6ff72fe9116521268f6f2dd4966f51df479883fe7037b39f75916ac3049d1a", "",
},
}
for i := range cases {
c := cases[i]
t.Run(c.label, func(t *testing.T) {
t.Parallel()
value, err := parser.ParseValue(strings.NewReader(c.input))
require.NoError(t, err)
f, err := canonicalCertFingerprint(value)
if c.err == "" {
require.NoError(t, err)
assert.Equal(t, ast.String(c.output), f)
} else {
assert.Equal(t, c.err, err.Error())
}
})
}
}
func TestSPKIHashFormatErrors(t *testing.T) {
t.Parallel()
cases := []struct {
label string
input string
err string
}{
{
"object",
`{}`, "certificate SPKI hash condition expects a string or array of strings",
},
{
"not base64",
`"not%valid%base64%data"`, "certificate SPKI hash must be a base64-encoded SHA-256 hash (was not%valid%base64%data)",
},
{
"SHA-1 hash",
`"VYby3BAoHawLLtsyckwo5Q=="`, "certificate SPKI hash must be a base64-encoded SHA-256 hash (was VYby3BAoHawLLtsyckwo5Q==)",
},
{
"valid",
`"FsDbM0rUYIiL3V339eIKqiz6HPSB+Pz2WeAWhqlqh8U="`, "",
},
}
for i := range cases {
c := cases[i]
t.Run(c.label, func(t *testing.T) {
t.Parallel()
value, err := parser.ParseValue(strings.NewReader(c.input))
require.NoError(t, err)
var body ast.Body
err = addCertSPKIHashCondition(&body, value)
if c.err == "" {
assert.NoError(t, err)
} else {
assert.Equal(t, c.err, err.Error())
}
})
}
}