616 lines
14 KiB
Go
Vendored
616 lines
14 KiB
Go
Vendored
package protocol
|
|
|
|
import (
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"math/big"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/go-webauthn/webauthn/protocol/webauthncbor"
|
|
"github.com/go-webauthn/webauthn/protocol/webauthncose"
|
|
)
|
|
|
|
func TestValidateRPID(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
value string
|
|
err string
|
|
}{
|
|
{
|
|
name: "ValidRPIDDomain",
|
|
value: "example.com",
|
|
},
|
|
{
|
|
name: "ValidRPIDLocalHost",
|
|
value: "localhost",
|
|
},
|
|
{
|
|
name: "ValidRPIDUsingIPv4",
|
|
value: "127.0.0.1",
|
|
},
|
|
{
|
|
name: "ValidRPIDUsingIPv4Alt",
|
|
value: "1.1.1.1",
|
|
},
|
|
{
|
|
name: "ValidRPIDUsingIPv6",
|
|
value: "2001:DB8:0:0:8:800:200C:417A",
|
|
},
|
|
{
|
|
name: "ValidRPIDUsingIPv6Alt",
|
|
value: "::1",
|
|
},
|
|
{
|
|
name: "InvalidRPIDNotDomain",
|
|
value: "example",
|
|
err: "the domain component must actually be a domain",
|
|
},
|
|
{
|
|
name: "InvalidRPIDScheme",
|
|
value: "https://example.com",
|
|
err: "the scheme component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDPort",
|
|
value: "example.com:1234",
|
|
err: "the port component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDPortWithScheme",
|
|
value: "https://example.com:1234",
|
|
err: "the port component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDPath",
|
|
value: "example.com/example",
|
|
err: "the path component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDQuery",
|
|
value: "example.com?abc=123",
|
|
err: "the query component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDFragment",
|
|
value: "example.com#abc=123",
|
|
err: "the fragment component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDPathWithScheme",
|
|
value: "https://example.com/example",
|
|
err: "the path component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDQueryWithScheme",
|
|
value: "https://example.com?abc=123",
|
|
err: "the query component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidRPIDFragmentWithScheme",
|
|
value: "https://example.com#abc=123",
|
|
err: "the fragment component must be empty",
|
|
},
|
|
{
|
|
name: "InvalidEmpty",
|
|
value: "",
|
|
err: "empty value provided",
|
|
},
|
|
{
|
|
name: "InvalidURI",
|
|
value: "https://example\x00.com",
|
|
err: "parse \"https://example\\x00.com\": net/url: invalid control character in URL",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := ValidateRPID(tc.value)
|
|
|
|
if tc.err == "" {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.EqualError(t, err, tc.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMustParseX509Certificate(t *testing.T) {
|
|
t.Run("ShouldPanic", func(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
mustParseX509Certificate([]byte("not a certificate"))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestMustParseX509CertificatePEM(t *testing.T) {
|
|
t.Run("ShouldPanicInvalidPEM", func(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
mustParseX509CertificatePEM([]byte("not a pem"))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestAttStatementParseX5CS(t *testing.T) {
|
|
cert := testUtilsGenerateSelfSignedCert(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
have map[string]any
|
|
expected struct {
|
|
count int
|
|
err string
|
|
}
|
|
}{
|
|
{
|
|
name: "ShouldFailNotArray",
|
|
have: map[string]any{"x5c": "not an array"},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
err: "Error retrieving x5c value",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailEmptyArray",
|
|
have: map[string]any{"x5c": []any{}},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
err: "Error retrieving x5c value: empty array",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailParseError",
|
|
have: map[string]any{"x5c": []any{[]byte("not a cert")}},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
err: "Error retrieving x5c value: error occurred parsing values",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldSucceed",
|
|
have: map[string]any{"x5c": []any{cert.Raw}},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
count: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
x5c, x5cs, err := attStatementParseX5CS(tc.have, "x5c")
|
|
|
|
if tc.expected.err == "" {
|
|
assert.NoError(t, err)
|
|
assert.Len(t, x5c, tc.expected.count)
|
|
assert.Len(t, x5cs, tc.expected.count)
|
|
} else {
|
|
assert.Nil(t, x5c)
|
|
assert.Nil(t, x5cs)
|
|
assert.EqualError(t, err, tc.expected.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseX5C(t *testing.T) {
|
|
cert := testUtilsGenerateSelfSignedCert(t)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
have []any
|
|
expected struct {
|
|
count int
|
|
err string
|
|
}
|
|
}{
|
|
{
|
|
name: "ShouldFailNotByteArray",
|
|
have: []any{"not bytes"},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
err: "x5c[0] is not a byte array",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailInvalidCert",
|
|
have: []any{[]byte("invalid cert der")},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
err: "x5c[0] is not a valid certificate: x509: malformed certificate",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldSucceed",
|
|
have: []any{cert.Raw},
|
|
expected: struct {
|
|
count int
|
|
err string
|
|
}{
|
|
count: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
x5cs, err := parseX5C(tc.have)
|
|
|
|
if tc.expected.err == "" {
|
|
assert.NoError(t, err)
|
|
assert.Len(t, x5cs, tc.expected.count)
|
|
} else {
|
|
assert.Nil(t, x5cs)
|
|
assert.EqualError(t, err, tc.expected.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAttStatementCertChainVerify(t *testing.T) {
|
|
ca := testUtilsGenerateSelfSignedCert(t)
|
|
leaf := testUtilsGenerateLeafCert(t, ca)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
have struct {
|
|
certs []*x509.Certificate
|
|
roots *x509.CertPool
|
|
}
|
|
expected struct {
|
|
empty bool
|
|
err string
|
|
}
|
|
}{
|
|
{
|
|
name: "ShouldFailEmptyChain",
|
|
have: struct {
|
|
certs []*x509.Certificate
|
|
roots *x509.CertPool
|
|
}{},
|
|
expected: struct {
|
|
empty bool
|
|
err string
|
|
}{
|
|
empty: true,
|
|
err: "empty chain",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldVerifyChainWithNilRoots",
|
|
have: struct {
|
|
certs []*x509.Certificate
|
|
roots *x509.CertPool
|
|
}{
|
|
certs: []*x509.Certificate{leaf, ca},
|
|
},
|
|
expected: struct {
|
|
empty bool
|
|
err string
|
|
}{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
chains, err := attStatementCertChainVerify(tc.have.certs, tc.have.roots, false, time.Time{})
|
|
|
|
if tc.expected.err == "" {
|
|
assert.NoError(t, err)
|
|
assert.NotEmpty(t, chains)
|
|
} else {
|
|
assert.EqualError(t, err, tc.expected.err)
|
|
|
|
if tc.expected.empty {
|
|
assert.Nil(t, chains)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifyAttestationECDSAPublicKeyMatch(t *testing.T) {
|
|
eccKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
coseKeyBytes, err := webauthncbor.Marshal(webauthncose.EC2PublicKeyData{
|
|
PublicKeyData: webauthncose.PublicKeyData{
|
|
KeyType: int64(webauthncose.EllipticKey),
|
|
Algorithm: int64(webauthncose.AlgES256),
|
|
},
|
|
Curve: int64(webauthncose.P256),
|
|
XCoord: padP256Coord(eccKey.X),
|
|
YCoord: padP256Coord(eccKey.Y),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
okpKeyBytes, err := webauthncbor.Marshal(webauthncose.OKPPublicKeyData{
|
|
PublicKeyData: webauthncose.PublicKeyData{
|
|
KeyType: int64(webauthncose.OctetKey),
|
|
Algorithm: int64(webauthncose.AlgEdDSA),
|
|
},
|
|
Curve: 1,
|
|
XCoord: make([]byte, 32),
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
matchingCert := testUtilsGenerateCertWithKey(t, &eccKey.PublicKey)
|
|
|
|
differentECCKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
differentCert := testUtilsGenerateCertWithKey(t, &differentECCKey.PublicKey)
|
|
|
|
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
rsaCert := testUtilsGenerateCertWithRSAKey(t, &rsaKey.PublicKey)
|
|
|
|
testCases := []struct {
|
|
name string
|
|
have struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}
|
|
expected struct {
|
|
algorithm int64
|
|
err string
|
|
}
|
|
}{
|
|
{
|
|
name: "ShouldSucceed",
|
|
have: struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}{
|
|
credentialPublicKey: coseKeyBytes,
|
|
cert: matchingCert,
|
|
},
|
|
expected: struct {
|
|
algorithm int64
|
|
err string
|
|
}{
|
|
algorithm: int64(webauthncose.AlgES256),
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailInvalidPublicKey",
|
|
have: struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}{
|
|
credentialPublicKey: []byte("invalid"),
|
|
cert: matchingCert,
|
|
},
|
|
expected: struct {
|
|
algorithm int64
|
|
err string
|
|
}{
|
|
err: "Error parsing public key: Unsupported Public Key Type",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailNotECDSAKey",
|
|
have: struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}{
|
|
credentialPublicKey: okpKeyBytes,
|
|
cert: matchingCert,
|
|
},
|
|
expected: struct {
|
|
algorithm int64
|
|
err string
|
|
}{
|
|
err: "Attestation public key is not ECDSA",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailCertNotECDSA",
|
|
have: struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}{
|
|
credentialPublicKey: coseKeyBytes,
|
|
cert: rsaCert,
|
|
},
|
|
expected: struct {
|
|
algorithm int64
|
|
err string
|
|
}{
|
|
err: "Credential public key is not ECDSA",
|
|
},
|
|
},
|
|
{
|
|
name: "ShouldFailKeyMismatch",
|
|
have: struct {
|
|
credentialPublicKey []byte
|
|
cert *x509.Certificate
|
|
}{
|
|
credentialPublicKey: coseKeyBytes,
|
|
cert: differentCert,
|
|
},
|
|
expected: struct {
|
|
algorithm int64
|
|
err string
|
|
}{
|
|
err: "Certificate public key does not match public key in authData",
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
att := AttestationObject{
|
|
AuthData: AuthenticatorData{
|
|
AttData: AttestedCredentialData{
|
|
CredentialPublicKey: tc.have.credentialPublicKey,
|
|
},
|
|
},
|
|
}
|
|
|
|
result, err := verifyAttestationECDSAPublicKeyMatch(att, tc.have.cert)
|
|
|
|
if tc.expected.err == "" {
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.expected.algorithm, result.Algorithm)
|
|
} else {
|
|
assert.EqualError(t, err, tc.expected.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func testUtilsGenerateSelfSignedCert(t *testing.T) *x509.Certificate {
|
|
t.Helper()
|
|
|
|
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "Test CA"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
IsCA: true,
|
|
KeyUsage: x509.KeyUsageCertSign,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := x509.ParseCertificate(der)
|
|
require.NoError(t, err)
|
|
|
|
return cert
|
|
}
|
|
|
|
func testUtilsGenerateLeafCert(t *testing.T, ca *x509.Certificate) *x509.Certificate {
|
|
t.Helper()
|
|
|
|
leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// We need the CA's private key to sign. Generate a new CA key pair for signing.
|
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
// Recreate the CA cert with the new key so we can sign the leaf.
|
|
caTemplate := &x509.Certificate{
|
|
SerialNumber: big.NewInt(1),
|
|
Subject: pkix.Name{CommonName: "Test CA"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
IsCA: true,
|
|
KeyUsage: x509.KeyUsageCertSign,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
|
|
caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
|
require.NoError(t, err)
|
|
|
|
caCert, err := x509.ParseCertificate(caDER)
|
|
require.NoError(t, err)
|
|
|
|
*ca = *caCert
|
|
|
|
leafTemplate := &x509.Certificate{
|
|
SerialNumber: big.NewInt(2),
|
|
Subject: pkix.Name{CommonName: "Test Leaf"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
}
|
|
|
|
leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCert, &leafKey.PublicKey, caKey)
|
|
require.NoError(t, err)
|
|
|
|
leafCert, err := x509.ParseCertificate(leafDER)
|
|
require.NoError(t, err)
|
|
|
|
return leafCert
|
|
}
|
|
|
|
func testUtilsGenerateCertWithKey(t *testing.T, pub *ecdsa.PublicKey) *x509.Certificate {
|
|
t.Helper()
|
|
|
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(2),
|
|
Subject: pkix.Name{CommonName: "Test Leaf"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
}
|
|
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := x509.ParseCertificate(der)
|
|
require.NoError(t, err)
|
|
|
|
return cert
|
|
}
|
|
|
|
// padP256Coord left-pads a big.Int coordinate to 32 bytes (the fixed width for P-256).
|
|
// big.Int.Bytes() drops leading zeroes, which would cause COSE EC2 key validation to
|
|
// reject coordinates shorter than 32 bytes.
|
|
func padP256Coord(v *big.Int) []byte {
|
|
const p256ByteLen = 32
|
|
|
|
b := v.Bytes()
|
|
|
|
if len(b) >= p256ByteLen {
|
|
return b
|
|
}
|
|
|
|
padded := make([]byte, p256ByteLen)
|
|
|
|
copy(padded[p256ByteLen-len(b):], b)
|
|
|
|
return padded
|
|
}
|
|
|
|
func testUtilsGenerateCertWithRSAKey(t *testing.T, pub *rsa.PublicKey) *x509.Certificate {
|
|
t.Helper()
|
|
|
|
caKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
require.NoError(t, err)
|
|
|
|
template := &x509.Certificate{
|
|
SerialNumber: big.NewInt(3),
|
|
Subject: pkix.Name{CommonName: "Test RSA Leaf"},
|
|
NotBefore: time.Now().Add(-time.Hour),
|
|
NotAfter: time.Now().Add(time.Hour),
|
|
}
|
|
|
|
der, err := x509.CreateCertificate(rand.Reader, template, template, pub, caKey)
|
|
require.NoError(t, err)
|
|
|
|
cert, err := x509.ParseCertificate(der)
|
|
require.NoError(t, err)
|
|
|
|
return cert
|
|
}
|