From fc47d9df9fbc0b35f363911dd0414e9fadd9229b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 29 Apr 2024 06:36:42 +1000 Subject: [PATCH 01/24] mds3 --- metadata/const.go | 32 + metadata/decode.go | 245 ++++++++ metadata/metadata.go | 1145 +++++++++++++++++++--------------- metadata/metadata_test.go | 216 ++++--- metadata/types.go | 267 ++++++++ protocol/attestation.go | 9 +- protocol/attestation_test.go | 8 +- 7 files changed, 1299 insertions(+), 623 deletions(-) create mode 100644 metadata/const.go create mode 100644 metadata/decode.go create mode 100644 metadata/types.go diff --git a/metadata/const.go b/metadata/const.go new file mode 100644 index 0000000..97ee93a --- /dev/null +++ b/metadata/const.go @@ -0,0 +1,32 @@ +package metadata + +import "github.com/google/uuid" + +const ( + // https://secure.globalsign.com/cacert/root-r3.crt + ProductionMDSRoot = "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpHWD9f" + + // Production MDS URL + ProductionMDSURL = "https://mds.fidoalliance.org" + + // https://mds3.fido.tools/pki/MDS3ROOT.crt + ConformanceMDSRoot = "MIICaDCCAe6gAwIBAgIPBCqih0DiJLW7+UHXx/o1MAoGCCqGSM49BAMDMGcxCzAJBgNVBAYTAlVTMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMScwJQYDVQQLDB5GQUtFIE1ldGFkYXRhIDMgQkxPQiBST09UIEZBS0UxFzAVBgNVBAMMDkZBS0UgUm9vdCBGQUtFMB4XDTE3MDIwMTAwMDAwMFoXDTQ1MDEzMTIzNTk1OVowZzELMAkGA1UEBhMCVVMxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxJzAlBgNVBAsMHkZBS0UgTWV0YWRhdGEgMyBCTE9CIFJPT1QgRkFLRTEXMBUGA1UEAwwORkFLRSBSb290IEZBS0UwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASKYiz3YltC6+lmxhPKwA1WFZlIqnX8yL5RybSLTKFAPEQeTD9O6mOz+tg8wcSdnVxHzwnXiQKJwhrav70rKc2ierQi/4QUrdsPes8TEirZOkCVJurpDFbXZOgs++pa4XmjYDBeMAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAfBgNVHSMEGDAWgBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAKBggqhkjOPQQDAwNoADBlAjEA/xFsgri0xubSa3y3v5ormpPqCwfqn9s0MLBAtzCIgxQ/zkzPKctkiwoPtDzI51KnAjAmeMygX2S5Ht8+e+EQnezLJBJXtnkRWY+Zt491wgt/AwSs5PHHMv5QgjELOuMxQBc=" + + // Example from https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html + ExampleMDSRoot = "MIIGGTCCBAGgAwIBAgIUdT9qLX0sVMRe8l0sLmHd3mZovQ0wDQYJKoZIhvcNAQELBQAwgZsxHzAdBgNVBAMMFkVYQU1QTEUgTURTMyBURVNUIFJPT1QxIjAgBgkqhkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xFDASBgNVBAoMC0V4YW1wbGUgT1JHMRAwDgYDVQQLDAdFeGFtcGxlMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0yMTA0MTkxMTM1MDdaFw00ODA5MDQxMTM1MDdaMIGbMR8wHQYDVQQDDBZFWEFNUExFIE1EUzMgVEVTVCBST09UMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUuY29tMRQwEgYDVQQKDAtFeGFtcGxlIE9SRzEQMA4GA1UECwwHRXhhbXBsZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDjF5wyEWuhwDHsZosGdGFTCcI677rW881vV+UfW38J+K2ioFFNeGVsxbcebK6AVOiCDPFj0974IpeD9SFOhwAHoDu/LCfXdQWp8ZgQ91ULYWoW8o7NNSp01nbN9zmaO6/xKNCa0bzjmXoGqglqnP1AtRcWYvXOSKZy1rcPeDv4Dhcpdp6W72fBw0eWIqOhsrItuY2/N8ItBPiG03EX72nACq4nZJ/nAIcUbER8STSFPPzvE97TvShsi1FD8aO6l1WkR/QkreAGjMI++GbB2Qc1nN9Y/VEDbMDhQtxXQRdpFwubTjejkN9hKOtF3B71YrwIrng3V9RoPMFdapWMzSlI+WWHog0oTj1PqwJDDg7+z1I6vSDeVWAMKr9mq1w1OGNzgBopIjd9lRWkRtt2kQSPX9XxqS4E1gDDr8MKbpM3JuubQtNCg9D7Ljvbz6vwvUrbPHH+oREvucsp0PZ5PpizloepGIcLFxDQqCulGY2n7Ahl0JOFXJqOFCaK3TWHwBvZsaY5DgBuUvdUrwtgZNg2eg2omWXEepiVFQn3Fvj43Wh2npPMgIe5P0rwncXvROxaczd4rtajKS1ucoB9b9iKqM2+M1y/FDIgVf1fWEHwK7YdzxMlgOeLdeV/kqRU5PEUlLU9a2EwdOErrPbPKZmIfbs/L4B3k4zejMDH3Y+ZwIDAQABo1MwUTAdBgNVHQ4EFgQU8sWwq1TrurK7xMTwO1dKfeJBbCMwHwYDVR0jBBgwFoAU8sWwq1TrurK7xMTwO1dKfeJBbCMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAFw6M1PiIfCPIBQ5EBUPNmRvRFuDpolOmDofnf/+mv63LqwQZAdo/W8tzZ9kOFhq24SiLw0H7fsdG/jeREXiIZMNoW/rA6Uac8sU+FYF7Q+qp6CQLlSQbDcpVMifTQjcBk2xh+aLK9SrrXBqnTAhwS+offGtAW8DpoLuH4tAcQmIjlgMlN65jnELCuqNR/wpA+zch8LZW8saQ2cwRCwdr8mAzZoLbsDSVCHxQF3/kQjPT7Nao1q2iWcY3OYcRmKrieHDP67yeLUbVmetfZis2d6ZlkqHLB4ZW1xX4otsEFkuTJA3HWDRsNyhTwx1YoCLsYut5Zp0myqPNBq28w6qGMyyoJN0Z4RzMEO3R6i/MQNfhK55/8O2HciM6xb5t/aBSuHPKlBDrFWhpRnKYkaNtlUo35qV5IbKGKau3SdZdSRciaXUd/p81YmoF01UlhhMz/Rqr1k2gyA0a9tF8+awCeanYt5izl8YO0FlrOU1SQ5UQw4szqqZqbrf4e8fRuU2TXNx4zk+ImE7WRB44f6mSD746ZCBRogZ/SA5jUBu+OPe4/sEtERWRcQD+fXgce9ZEN0+peyJIKAsl5Rm2Bmgyg5IoyWwSG5W+WekGyEokpslou2Yc6EjUj5ndZWz5EiHAiQ74hNfDoCZIxVVLU3Qbp8a0S1bmsoT2JOsspIbtZUg=" +) + +var ( + // Conformance indicates if test metadata is currently being used + Conformance = false + + MDSRoot = ProductionMDSRoot +) + +const ( + HeaderX509URI = "x5u" + HeaderX509Certificate = "x5c" +) + +// Metadata is a map of authenticator AAGUIDs to corresponding metadata statements +var Metadata = make(map[uuid.UUID]MetadataBLOBPayloadEntry) diff --git a/metadata/decode.go b/metadata/decode.go new file mode 100644 index 0000000..e3232b4 --- /dev/null +++ b/metadata/decode.go @@ -0,0 +1,245 @@ +package metadata + +import ( + "crypto/x509" + "encoding/base64" + "errors" + "fmt" + "net/http" + + "github.com/go-webauthn/x/revoke" + "github.com/golang-jwt/jwt/v5" + "github.com/mitchellh/mapstructure" +) + +// NewDecoder returns a new metadata decoder. +func NewDecoder() (decoder *Decoder) { + return &Decoder{ + client: &http.Client{}, + parser: jwt.NewParser(), + hook: mapstructure.ComposeDecodeHookFunc(), + } +} + +type Decoder struct { + client *http.Client + parser *jwt.Parser + hook mapstructure.DecodeHookFunc +} + +func (d *Decoder) Decode(blob []byte) (payload *MetadataBLOBPayloadJSON, err error) { + payload = &MetadataBLOBPayloadJSON{} + + var token *jwt.Token + + if token, err = d.parser.Parse(string(blob), func(token *jwt.Token) (any, error) { + // 2. If the x5u attribute is present in the JWT Header, then + if _, ok := token.Header[HeaderX509URI].([]any); ok { + // never seen an x5u here, although it is in the spec + return nil, errors.New("x5u encountered in header of metadata TOC payload") + } + + // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. + var ( + x5c, chain []any + ok, valid bool + ) + + if x5c, ok = token.Header[HeaderX509Certificate].([]any); !ok { + // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. + chain[0] = MDSRoot + } else { + chain = x5c + } + + // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. + if valid, err = validateChain(chain, d.client); !valid || err != nil { + return nil, err + } + + // Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the + // certificate bytes. + o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) + + var ( + n int + cert *x509.Certificate + ) + + // Decode the base64 certificate into the buffer. + if n, err = base64.StdEncoding.Decode(o, []byte(chain[0].(string))); err != nil { + return nil, err + } + + // Parse the certificate from the buffer. + if cert, err = x509.ParseCertificate(o[:n]); err != nil { + return nil, err + } + + // 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain + // jwt.Parse() uses the TOC signing certificate public key internally to verify the signature. + return cert.PublicKey, err + }); err != nil { + return nil, err + } + + var decoder *mapstructure.Decoder + + if decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Metadata: nil, + Result: payload, + DecodeHook: d.hook, + }); err != nil { + return nil, err + } + + if err = decoder.Decode(token.Claims); err != nil { + return payload, err + } + + return payload, nil +} + +func unmarshalMDSBLOB(body []byte, c *http.Client) (MetadataBLOBPayloadJSON, error) { + var payload MetadataBLOBPayloadJSON + + token, err := jwt.Parse(string(body), func(token *jwt.Token) (any, error) { + // 2. If the x5u attribute is present in the JWT Header, then + if _, ok := token.Header[HeaderX509URI].([]any); ok { + // never seen an x5u here, although it is in the spec + return nil, errors.New("x5u encountered in header of metadata TOC payload") + } + var chain []any + // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. + + if x5c, ok := token.Header[HeaderX509Certificate].([]any); !ok { + // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. + chain[0] = MDSRoot + } else { + chain = x5c + } + + // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. + valid, err := validateChain(chain, c) + if !valid || err != nil { + return nil, err + } + + // Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the + // certificate bytes. + o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) + + // base64 decode the certificate into the buffer. + n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string))) + if err != nil { + return nil, err + } + + // Parse the certificate from the buffer. + cert, err := x509.ParseCertificate(o[:n]) + if err != nil { + return nil, err + } + + // 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain + // jwt.Parse() uses the TOC signing certificate public key internally to verify the signature. + return cert.PublicKey, err + }) + + if err != nil { + return payload, err + } + + err = mapstructure.Decode(token.Claims, &payload) + + return payload, err +} + +func validateChain(chain []any, c *http.Client) (bool, error) { + oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(MDSRoot))) + + nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(MDSRoot)) + if err != nil { + return false, err + } + + rootcert, err := x509.ParseCertificate(oRoot[:nRoot]) + if err != nil { + return false, err + } + + roots := x509.NewCertPool() + + roots.AddCert(rootcert) + + o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[1].(string)))) + + n, err := base64.StdEncoding.Decode(o, []byte(chain[1].(string))) + if err != nil { + return false, err + } + + intcert, err := x509.ParseCertificate(o[:n]) + if err != nil { + return false, err + } + + if revoked, ok := revoke.VerifyCertificate(intcert); !ok { + issuer := intcert.IssuingCertificateURL + + if issuer != nil { + return false, errCRLUnavailable + } + } else if revoked { + return false, errIntermediateCertRevoked + } + + ints := x509.NewCertPool() + ints.AddCert(intcert) + + l := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) + + n, err = base64.StdEncoding.Decode(l, []byte(chain[0].(string))) + if err != nil { + return false, err + } + + leafcert, err := x509.ParseCertificate(l[:n]) + if err != nil { + return false, err + } + + if revoked, ok := revoke.VerifyCertificate(leafcert); !ok { + return false, errCRLUnavailable + } else if revoked { + return false, errLeafCertRevoked + } + + opts := x509.VerifyOptions{ + Roots: roots, + Intermediates: ints, + } + + _, err = leafcert.Verify(opts) + + return err == nil, err +} + +func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, err error) { + var n int + + raw := make([]byte, base64.StdEncoding.DecodedLen(len(value))) + + if n, err = base64.StdEncoding.Decode(raw, []byte(value)); err != nil { + return nil, fmt.Errorf("error occurred parsing *x509.certificate: error occurred decoding base64 data: %w", err) + } + + if certificate, err = x509.ParseCertificate(raw[:n]); err != nil { + fmt.Println(err) + + return nil, nil + //return nil, fmt.Errorf("error occurred parsing *x509.certificate: error occurred parsing certificate: %w", err) + } + + return certificate, nil +} diff --git a/metadata/metadata.go b/metadata/metadata.go index d97d809..147e054 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -2,533 +2,832 @@ package metadata import ( "crypto/x509" - "encoding/base64" - "errors" - "io" - "net/http" - "reflect" + "fmt" + "net/url" "time" - "github.com/golang-jwt/jwt/v5" "github.com/google/uuid" - "github.com/mitchellh/mapstructure" +) + +// MetadataBLOBPayload is a structure representing the MetadataBLOBPayload MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary +type MetadataBLOBPayload struct { + // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. + LegalHeader string - "github.com/go-webauthn/x/revoke" + // The serial number of this UAF Metadata TOC Payload. Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor TOC will have a no value exactly incremented by one. + Number int - "github.com/go-webauthn/webauthn/protocol/webauthncose" -) + // ISO-8601 formatted date when the next update will be provided at latest. + NextUpdate time.Time -type PublicKeyCredentialParameters struct { - Type string `json:"type"` - Alg webauthncose.COSEAlgorithmIdentifier `json:"alg"` + // List of zero or more MetadataTOCPayloadEntry objects. + Entries []MetadataBLOBPayloadEntry } -const ( - // https://secure.globalsign.com/cacert/root-r3.crt - ProductionMDSRoot = "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpHWD9f" - // Production MDS URL - ProductionMDSURL = "https://mds.fidoalliance.org" - // https://mds3.fido.tools/pki/MDS3ROOT.crt - ConformanceMDSRoot = "MIICaDCCAe6gAwIBAgIPBCqih0DiJLW7+UHXx/o1MAoGCCqGSM49BAMDMGcxCzAJBgNVBAYTAlVTMRYwFAYDVQQKDA1GSURPIEFsbGlhbmNlMScwJQYDVQQLDB5GQUtFIE1ldGFkYXRhIDMgQkxPQiBST09UIEZBS0UxFzAVBgNVBAMMDkZBS0UgUm9vdCBGQUtFMB4XDTE3MDIwMTAwMDAwMFoXDTQ1MDEzMTIzNTk1OVowZzELMAkGA1UEBhMCVVMxFjAUBgNVBAoMDUZJRE8gQWxsaWFuY2UxJzAlBgNVBAsMHkZBS0UgTWV0YWRhdGEgMyBCTE9CIFJPT1QgRkFLRTEXMBUGA1UEAwwORkFLRSBSb290IEZBS0UwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASKYiz3YltC6+lmxhPKwA1WFZlIqnX8yL5RybSLTKFAPEQeTD9O6mOz+tg8wcSdnVxHzwnXiQKJwhrav70rKc2ierQi/4QUrdsPes8TEirZOkCVJurpDFbXZOgs++pa4XmjYDBeMAsGA1UdDwQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAfBgNVHSMEGDAWgBQGcfeCs0Y8D+lh6U5B2xSrR74eHTAKBggqhkjOPQQDAwNoADBlAjEA/xFsgri0xubSa3y3v5ormpPqCwfqn9s0MLBAtzCIgxQ/zkzPKctkiwoPtDzI51KnAjAmeMygX2S5Ht8+e+EQnezLJBJXtnkRWY+Zt491wgt/AwSs5PHHMv5QgjELOuMxQBc=" - // Example from https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html - ExampleMDSRoot = "MIIGGTCCBAGgAwIBAgIUdT9qLX0sVMRe8l0sLmHd3mZovQ0wDQYJKoZIhvcNAQELBQAwgZsxHzAdBgNVBAMMFkVYQU1QTEUgTURTMyBURVNUIFJPT1QxIjAgBgkqhkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xFDASBgNVBAoMC0V4YW1wbGUgT1JHMRAwDgYDVQQLDAdFeGFtcGxlMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0yMTA0MTkxMTM1MDdaFw00ODA5MDQxMTM1MDdaMIGbMR8wHQYDVQQDDBZFWEFNUExFIE1EUzMgVEVTVCBST09UMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUuY29tMRQwEgYDVQQKDAtFeGFtcGxlIE9SRzEQMA4GA1UECwwHRXhhbXBsZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDjF5wyEWuhwDHsZosGdGFTCcI677rW881vV+UfW38J+K2ioFFNeGVsxbcebK6AVOiCDPFj0974IpeD9SFOhwAHoDu/LCfXdQWp8ZgQ91ULYWoW8o7NNSp01nbN9zmaO6/xKNCa0bzjmXoGqglqnP1AtRcWYvXOSKZy1rcPeDv4Dhcpdp6W72fBw0eWIqOhsrItuY2/N8ItBPiG03EX72nACq4nZJ/nAIcUbER8STSFPPzvE97TvShsi1FD8aO6l1WkR/QkreAGjMI++GbB2Qc1nN9Y/VEDbMDhQtxXQRdpFwubTjejkN9hKOtF3B71YrwIrng3V9RoPMFdapWMzSlI+WWHog0oTj1PqwJDDg7+z1I6vSDeVWAMKr9mq1w1OGNzgBopIjd9lRWkRtt2kQSPX9XxqS4E1gDDr8MKbpM3JuubQtNCg9D7Ljvbz6vwvUrbPHH+oREvucsp0PZ5PpizloepGIcLFxDQqCulGY2n7Ahl0JOFXJqOFCaK3TWHwBvZsaY5DgBuUvdUrwtgZNg2eg2omWXEepiVFQn3Fvj43Wh2npPMgIe5P0rwncXvROxaczd4rtajKS1ucoB9b9iKqM2+M1y/FDIgVf1fWEHwK7YdzxMlgOeLdeV/kqRU5PEUlLU9a2EwdOErrPbPKZmIfbs/L4B3k4zejMDH3Y+ZwIDAQABo1MwUTAdBgNVHQ4EFgQU8sWwq1TrurK7xMTwO1dKfeJBbCMwHwYDVR0jBBgwFoAU8sWwq1TrurK7xMTwO1dKfeJBbCMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAFw6M1PiIfCPIBQ5EBUPNmRvRFuDpolOmDofnf/+mv63LqwQZAdo/W8tzZ9kOFhq24SiLw0H7fsdG/jeREXiIZMNoW/rA6Uac8sU+FYF7Q+qp6CQLlSQbDcpVMifTQjcBk2xh+aLK9SrrXBqnTAhwS+offGtAW8DpoLuH4tAcQmIjlgMlN65jnELCuqNR/wpA+zch8LZW8saQ2cwRCwdr8mAzZoLbsDSVCHxQF3/kQjPT7Nao1q2iWcY3OYcRmKrieHDP67yeLUbVmetfZis2d6ZlkqHLB4ZW1xX4otsEFkuTJA3HWDRsNyhTwx1YoCLsYut5Zp0myqPNBq28w6qGMyyoJN0Z4RzMEO3R6i/MQNfhK55/8O2HciM6xb5t/aBSuHPKlBDrFWhpRnKYkaNtlUo35qV5IbKGKau3SdZdSRciaXUd/p81YmoF01UlhhMz/Rqr1k2gyA0a9tF8+awCeanYt5izl8YO0FlrOU1SQ5UQw4szqqZqbrf4e8fRuU2TXNx4zk+ImE7WRB44f6mSD746ZCBRogZ/SA5jUBu+OPe4/sEtERWRcQD+fXgce9ZEN0+peyJIKAsl5Rm2Bmgyg5IoyWwSG5W+WekGyEokpslou2Yc6EjUj5ndZWz5EiHAiQ74hNfDoCZIxVVLU3Qbp8a0S1bmsoT2JOsspIbtZUg=" -) +// MetadataBLOBPayloadJSON is an intermediary JSON/JWT representation of the MetadataBLOBPayload. +type MetadataBLOBPayloadJSON struct { + LegalHeader string `json:"legalHeader"` + Number int `json:"no"` + NextUpdate string `json:"nextUpdate"` + + Entries []MetadataBLOBPayloadEntryJSON `json:"entries"` +} -// Metadata is a map of authenticator AAGUIDs to corresponding metadata statements -var Metadata = make(map[uuid.UUID]MetadataBLOBPayloadEntry) +func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error) { + var update time.Time -// Conformance indicates if test metadata is currently being used -var Conformance = false + if update, err = time.Parse(time.DateOnly, j.NextUpdate); err != nil { + return payload, fmt.Errorf("error occurred parsing next update value: %w", err) + } + + n := len(j.Entries) + + entries := make([]MetadataBLOBPayloadEntry, n) + + for i := 0; i < n; i++ { + if entries[i], err = j.Entries[i].Parse(); err != nil { + return payload, fmt.Errorf("error occurred parsing entry %d: %w", i, err) + } + } -var MDSRoot = ProductionMDSRoot + return MetadataBLOBPayload{ + LegalHeader: j.LegalHeader, + Number: j.Number, + NextUpdate: update, + Entries: entries, + }, nil +} -// MetadataBLOBPayloadEntry - Represents the MetadataBLOBPayloadEntry -// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary +// MetadataBLOBPayloadEntry is a structure representing the MetadataBLOBPayloadEntry MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary type MetadataBLOBPayloadEntry struct { // The Authenticator Attestation ID. Aaid string `json:"aaid"` + // The Authenticator Attestation GUID. - AaGUID string `json:"aaguid"` + AaGUID uuid.UUID `json:"aaguid"` + // A list of the attestation certificate public key identifiers encoded as hex string. AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` + // The metadataStatement JSON object as defined in FIDOMetadataStatement. MetadataStatement MetadataStatement `json:"metadataStatement"` + // Status of the FIDO Biometric Certification of one or more biometric components of the Authenticator BiometricStatusReports []BiometricStatusReport `json:"biometricStatusReports"` + // An array of status reports applicable to this authenticator. StatusReports []StatusReport `json:"statusReports"` + // ISO-8601 formatted date since when the status report array was set to the current value. - TimeOfLastStatusChange string `json:"timeOfLastStatusChange"` + TimeOfLastStatusChange time.Time + // URL of a list of rogue (i.e. untrusted) individual authenticators. - RogueListURL string `json:"rogueListURL"` + RogueListURL *url.URL + // The hash value computed over the Base64url encoding of the UTF-8 representation of the JSON encoded rogueList available at rogueListURL (with type rogueListEntry[]). - RogueListHash string `json:"rogueListHash"` + RogueListHash string +} + +// MetadataBLOBPayloadEntryJSON is an intermediary JSON/JWT structure representing the MetadataBLOBPayloadEntry MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary +type MetadataBLOBPayloadEntryJSON struct { + Aaid string `json:"aaid"` + AaGUID string `json:"aaguid"` + AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` + + MetadataStatement MetadataStatementJSON `json:"metadataStatement"` + BiometricStatusReports []BiometricStatusReportJSON `json:"biometricStatusReports"` + StatusReports []StatusReportJSON `json:"statusReports"` + + TimeOfLastStatusChange string `json:"timeOfLastStatusChange"` + RogueListURL string `json:"rogueListURL"` + RogueListHash string `json:"rogueListHash"` } -// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#biometricstatusreport-dictionary -// BiometricStatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component. +func (j MetadataBLOBPayloadEntryJSON) Parse() (entry MetadataBLOBPayloadEntry, err error) { + var aaguid uuid.UUID + + if len(j.AaGUID) != 0 { + if aaguid, err = uuid.Parse(j.AaGUID); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error parsing AAGUID: %w", j.AaGUID, err) + } + } + + var statement MetadataStatement + + if statement, err = j.MetadataStatement.Parse(); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': %w", j.AaGUID, err) + } + + var i, n int + + n = len(j.BiometricStatusReports) + + bsrs := make([]BiometricStatusReport, n) + + for i = 0; i < n; i++ { + if bsrs[i], err = j.BiometricStatusReports[i].Parse(); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error occurred parsing biometric status report %d: %w", j.AaGUID, i, err) + } + } + + n = len(j.StatusReports) + + srs := make([]StatusReport, n) + + for i = 0; i < n; i++ { + if srs[i], err = j.StatusReports[i].Parse(); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error occurred parsing status report %d: %w", j.AaGUID, i, err) + } + } + + var change time.Time + + if change, err = time.Parse(time.DateOnly, j.TimeOfLastStatusChange); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error occurred parsing time of last status change value: %w", j.AaGUID, err) + } + + var rogues *url.URL + + if len(j.RogueListURL) != 0 { + if rogues, err = url.ParseRequestURI(j.RogueListURL); err != nil { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error occurred parsing rogue list URL value: %w", j.AaGUID, err) + } + + if len(j.RogueListHash) == 0 { + return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': error occurred validating rogue list URL value: the rogue list hash was absent", j.AaGUID) + } + } + + return MetadataBLOBPayloadEntry{ + Aaid: j.Aaid, + AaGUID: aaguid, + AttestationCertificateKeyIdentifiers: j.AttestationCertificateKeyIdentifiers, + MetadataStatement: statement, + BiometricStatusReports: bsrs, + StatusReports: srs, + TimeOfLastStatusChange: change, + RogueListURL: rogues, + RogueListHash: j.RogueListHash, + }, nil +} + +// MetadataStatement is a structure representing the MetadataStatement MDS3 dictionary. +// Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information +// contained in the authoritative statement is used in several other places. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#metadata-keys +type MetadataStatement struct { + // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. + LegalHeader string + + // The Authenticator Attestation ID. + Aaid string + + // The Authenticator Attestation GUID. + AaGUID uuid.UUID + + // A list of the attestation certificate public key identifiers encoded as hex string. + AttestationCertificateKeyIdentifiers []string + + // A human-readable, short description of the authenticator, in English. + Description string + + // A list of human-readable short descriptions of the authenticator in different languages. + AlternativeDescriptions map[string]string + + // Earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement. + AuthenticatorVersion uint32 + + // The FIDO protocol family. The values "uaf", "u2f", and "fido2" are supported. + ProtocolFamily string + + // The FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator. + Upv []Version + + // The list of authentication algorithms supported by the authenticator. + AuthenticationAlgorithms []AuthenticationAlgorithm + + // The list of public key formats supported by the authenticator during registration operations. + PublicKeyAlgAndEncodings []PublicKeyAlgAndEncoding + + // The supported attestation type(s). + AttestationTypes []AuthenticatorAttestationType + + // A list of alternative VerificationMethodANDCombinations. + UserVerificationDetails [][]VerificationMethodDescriptor + + // A 16-bit number representing the bit fields defined by the KEY_PROTECTION constants in the FIDO Registry of Predefined Values + KeyProtection []string + + // This entry is set to true or it is omitted, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. + // This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions. + IsKeyRestricted bool + + // This entry is set to true or it is omitted, if Uauth key usage always requires a fresh user verification + // This entry is set to false, if the Uauth key can be used without requiring a fresh user verification, e.g. without any additional user interaction, if the user was verified a (potentially configurable) caching time ago. + IsFreshUserVerificationRequired bool + + // A 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants in the FIDO Registry of Predefined Values + MatcherProtection []string + + // The authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level). + CryptoStrength uint16 + + // A 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants in the FIDO Registry of Predefined Values + AttachmentHint []string + + // A 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants in the FIDO Registry of Predefined Values + TcDisplay []string + + // Supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png. + TcDisplayContentType string + + // A list of alternative DisplayPNGCharacteristicsDescriptor. Each of these entries is one alternative of supported image characteristics for displaying a PNG image. + TcDisplayPNGCharacteristics []DisplayPNGCharacteristicsDescriptor + + // Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. + // Multiple certificates might be used for different batches of the same model. + // The array does not represent a certificate chain, but only the trust anchor of that chain. + // A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself. + AttestationRootCertificates []*x509.Certificate + + // A list of trust anchors used for ECDAA attestation. This entry MUST be present if and only if attestationType includes ATTESTATION_ECDAA. + EcdaaTrustAnchors []EcdaaTrustAnchor + + // A data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator. + Icon *url.URL + + // List of extensions supported by the authenticator. + SupportedExtensions []ExtensionDescriptor + + // Describes supported versions, extensions, AAGUID of the device and its capabilities + AuthenticatorGetInfo AuthenticatorGetInfo +} + +func (s *MetadataStatement) Verifier() (opts x509.VerifyOptions) { + roots := x509.NewCertPool() + + for _, root := range s.AttestationRootCertificates { + roots.AddCert(root) + } + + return x509.VerifyOptions{ + Roots: roots, + } +} + +// MetadataStatementJSON is an intermediary JSON/JWT structure representing the MetadataStatement MDS3 dictionary. +// Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information +// contained in the authoritative statement is used in several other places. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#metadata-keys +type MetadataStatementJSON struct { + LegalHeader string `json:"legalHeader"` + Aaid string `json:"aaid"` + AaGUID string `json:"aaguid"` + AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` + Description string `json:"description"` + AlternativeDescriptions map[string]string `json:"alternativeDescriptions"` + AuthenticatorVersion uint32 `json:"authenticatorVersion"` + ProtocolFamily string `json:"protocolFamily"` + Upv []Version `json:"upv"` + AuthenticationAlgorithms []AuthenticationAlgorithm `json:"authenticationAlgorithms"` + PublicKeyAlgAndEncodings []PublicKeyAlgAndEncoding `json:"publicKeyAlgAndEncodings"` + AttestationTypes []AuthenticatorAttestationType `json:"attestationTypes"` + UserVerificationDetails [][]VerificationMethodDescriptor `json:"userVerificationDetails"` + KeyProtection []string `json:"keyProtection"` + IsKeyRestricted bool `json:"isKeyRestricted"` + IsFreshUserVerificationRequired bool `json:"isFreshUserVerificationRequired"` + MatcherProtection []string `json:"matcherProtection"` + CryptoStrength uint16 `json:"cryptoStrength"` + AttachmentHint []string `json:"attachmentHint"` + TcDisplay []string `json:"tcDisplay"` + TcDisplayContentType string `json:"tcDisplayContentType"` + TcDisplayPNGCharacteristics []DisplayPNGCharacteristicsDescriptor `json:"tcDisplayPNGCharacteristics"` + AttestationRootCertificates []string `json:"attestationRootCertificates"` + EcdaaTrustAnchors []EcdaaTrustAnchor `json:"ecdaaTrustAnchors"` + Icon string `json:"icon"` + SupportedExtensions []ExtensionDescriptor `json:"supportedExtensions"` + AuthenticatorGetInfo AuthenticatorGetInfoJSON `json:"authenticatorGetInfo"` +} + +func (j MetadataStatementJSON) Parse() (statement MetadataStatement, err error) { + var aaguid uuid.UUID + + if len(j.AaGUID) != 0 { + if aaguid, err = uuid.Parse(j.AaGUID); err != nil { + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occured parsing AAGUID value: %w", j.Description, err) + } + } + + n := len(j.AttestationRootCertificates) + + certificates := make([]*x509.Certificate, n) + + for i := 0; i < n; i++ { + if certificates[i], err = ParseMetadataX509Certificate(j.AttestationRootCertificates[i]); err != nil { + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing attestation root certificate %d value: %w", j.Description, i, err) + } + } + + var icon *url.URL + + if icon, err = url.ParseRequestURI(j.Icon); err != nil { + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing icon value: %w", j.Description, err) + } + + var info AuthenticatorGetInfo + + if info, err = j.AuthenticatorGetInfo.Parse(); err != nil { + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing authenticator get info value: %w", j.Description, err) + } + + return MetadataStatement{ + LegalHeader: j.LegalHeader, + Aaid: j.Aaid, + AaGUID: aaguid, + AttestationCertificateKeyIdentifiers: j.AttestationCertificateKeyIdentifiers, + Description: j.Description, + AlternativeDescriptions: j.AlternativeDescriptions, + AuthenticatorVersion: j.AuthenticatorVersion, + ProtocolFamily: j.ProtocolFamily, + Upv: j.Upv, + AuthenticationAlgorithms: j.AuthenticationAlgorithms, + PublicKeyAlgAndEncodings: j.PublicKeyAlgAndEncodings, + AttestationTypes: j.AttestationTypes, + UserVerificationDetails: j.UserVerificationDetails, + KeyProtection: j.KeyProtection, + IsKeyRestricted: j.IsKeyRestricted, + IsFreshUserVerificationRequired: j.IsFreshUserVerificationRequired, + MatcherProtection: j.MatcherProtection, + CryptoStrength: j.CryptoStrength, + AttachmentHint: j.AttachmentHint, + TcDisplay: j.TcDisplay, + TcDisplayContentType: j.TcDisplayContentType, + TcDisplayPNGCharacteristics: j.TcDisplayPNGCharacteristics, + AttestationRootCertificates: certificates, + EcdaaTrustAnchors: j.EcdaaTrustAnchors, + Icon: icon, + SupportedExtensions: j.SupportedExtensions, + AuthenticatorGetInfo: info, + }, nil +} + +// BiometricStatusReport is a structure representing the BiometricStatusReport MDS3 dictionary. +// Contains the current status of the authenticator's biometric component. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#biometricstatusreport-dictionary type BiometricStatusReport struct { // Achieved level of the biometric certification of this biometric component of the authenticator - CertLevel uint16 `json:"certLevel"` + CertLevel uint16 + // A single USER_VERIFY constant indicating the modality of the biometric component - Modality string `json:"modality"` + Modality string + // ISO-8601 formatted date since when the certLevel achieved, if applicable. If no date is given, the status is assumed to be effective while present. - EffectiveDate string `json:"effectiveDate"` + EffectiveDate time.Time + // Describes the externally visible aspects of the Biometric Certification evaluation. - CertificationDescriptor string `json:"certificationDescriptor"` + CertificationDescriptor string + // The unique identifier for the issued Biometric Certification. - CertificateNumber string `json:"certificateNumber"` + CertificateNumber string + // The version of the Biometric Certification Policy the implementation is Certified to, e.g. "1.0.0". - CertificationPolicyVersion string `json:"certificationPolicyVersion"` + CertificationPolicyVersion string + // The version of the Biometric Requirements [FIDOBiometricsRequirements] the implementation is certified to, e.g. "1.0.0". + CertificationRequirementsVersion string +} + +// BiometricStatusReportJSON is a structure representing the BiometricStatusReport MDS3 dictionary. +// Contains the current status of the authenticator's biometric component. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#biometricstatusreport-dictionary +type BiometricStatusReportJSON struct { + CertLevel uint16 `json:"certLevel"` + Modality string `json:"modality"` + EffectiveDate string `json:"effectiveDate"` + CertificationDescriptor string `json:"certificationDescriptor"` + CertificateNumber string `json:"certificateNumber"` + + CertificationPolicyVersion string `json:"certificationPolicyVersion"` CertificationRequirementsVersion string `json:"certificationRequirementsVersion"` } -// StatusReport - Contains the current BiometricStatusReport of one of the authenticator's biometric component. -// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#statusreport-dictionary +func (j BiometricStatusReportJSON) Parse() (report BiometricStatusReport, err error) { + var effective time.Time + + if effective, err = time.Parse(time.DateOnly, j.EffectiveDate); err != nil { + return report, fmt.Errorf("error occurred parsing effective date value: %w", err) + } + + return BiometricStatusReport{ + CertLevel: j.CertLevel, + Modality: j.Modality, + EffectiveDate: effective, + CertificationDescriptor: j.CertificationDescriptor, + CertificateNumber: j.CertificateNumber, + CertificationPolicyVersion: j.CertificationPolicyVersion, + CertificationRequirementsVersion: j.CertificationRequirementsVersion, + }, nil +} + +// StatusReport is a structure representing the StatusReport MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#statusreport-dictionary type StatusReport struct { // Status of the authenticator. Additional fields MAY be set depending on this value. - Status AuthenticatorStatus `json:"status"` + Status AuthenticatorStatus + // ISO-8601 formatted date since when the status code was set, if applicable. If no date is given, the status is assumed to be effective while present. - EffectiveDate string `json:"effectiveDate"` + EffectiveDate time.Time + // The authenticatorVersion that this status report relates to. In the case of FIDO_CERTIFIED* status values, the status applies to higher authenticatorVersions until there is a new statusReport. - AuthenticatorVersion uint32 `json:"authenticatorVersion"` + AuthenticatorVersion uint32 + // Base64-encoded [RFC4648] (not base64url!) DER [ITU-X690-2008] PKIX certificate value related to the current status, if applicable. - Certificate string `json:"certificate"` + Certificate *x509.Certificate + // HTTPS URL where additional information may be found related to the current status, if applicable. - URL string `json:"url"` + URL *url.URL + // Describes the externally visible aspects of the Authenticator Certification evaluation. - CertificationDescriptor string `json:"certificationDescriptor"` + CertificationDescriptor string + // The unique identifier for the issued Certification. - CertificateNumber string `json:"certificateNumber"` + CertificateNumber string + // The version of the Authenticator Certification Policy the implementation is Certified to, e.g. "1.0.0". - CertificationPolicyVersion string `json:"certificationPolicyVersion"` + CertificationPolicyVersion string + // The Document Version of the Authenticator Security Requirements (DV) [FIDOAuthenticatorSecurityRequirements] the implementation is certified to, e.g. "1.2.0". - CertificationRequirementsVersion string `json:"certificationRequirementsVersion"` + CertificationRequirementsVersion string } -// AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports. -// Each constant has a case-sensitive string representation (in quotes), which is used in the authoritative metadata for FIDO authenticators. -type AuthenticatorAttestationType string - -const ( - // BasicFull - Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). Authenticators must provide its attestation signature during the registration process for the same reason. The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). This sharing process should be done according to [UAFMetadataService]. - BasicFull AuthenticatorAttestationType = "basic_full" - // BasicSurrogate - Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. As a consequence it does not provide a cryptographic proof of the security characteristics. But it is the best thing we can do if the authenticator is not able to have an attestation private key. - BasicSurrogate AuthenticatorAttestationType = "basic_surrogate" - // Ecdaa - Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. - Ecdaa AuthenticatorAttestationType = "ecdaa" - // AttCA - Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. - AttCA AuthenticatorAttestationType = "attca" - // AnonCA In this case, the authenticator uses an Anonymization CA which dynamically generates per-credential attestation certificates such that the attestation statements presented to Relying Parties do not provide uniquely identifiable information, e.g., that might be used for tracking purposes. The applicable [WebAuthn] attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application Attestation "apple-appattest". - AnonCA AuthenticatorAttestationType = "anonca" - // None - Indicates absence of attestation - None AuthenticatorAttestationType = "none" -) +// StatusReportJSON is an intermediary JSON/JWT structure representing the StatusReport MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#statusreport-dictionary +type StatusReportJSON struct { + Status AuthenticatorStatus `json:"status"` + EffectiveDate string `json:"effectiveDate"` + AuthenticatorVersion uint32 `json:"authenticatorVersion"` + Certificate string `json:"certificate"` + URL string `json:"url"` + CertificationDescriptor string `json:"certificationDescriptor"` + CertificateNumber string `json:"certificateNumber"` + CertificationPolicyVersion string `json:"certificationPolicyVersion"` + CertificationRequirementsVersion string `json:"certificationRequirementsVersion"` +} -// AuthenticatorStatus - This enumeration describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key). -// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#authenticatorstatus-enum -type AuthenticatorStatus string - -const ( - // NotFidoCertified - This authenticator is not FIDO certified. - NotFidoCertified AuthenticatorStatus = "NOT_FIDO_CERTIFIED" - // FidoCertified - This authenticator has passed FIDO functional certification. This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1. - FidoCertified AuthenticatorStatus = "FIDO_CERTIFIED" - // UserVerificationBypass - Indicates that malware is able to bypass the user verification. This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge. - UserVerificationBypass AuthenticatorStatus = "USER_VERIFICATION_BYPASS" - // AttestationKeyCompromise - Indicates that an attestation key for this authenticator is known to be compromised. Additional data should be supplied, including the key identifier and the date of compromise, if known. - AttestationKeyCompromise AuthenticatorStatus = "ATTESTATION_KEY_COMPROMISE" - // UserKeyRemoteCompromise - This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted. - UserKeyRemoteCompromise AuthenticatorStatus = "USER_KEY_REMOTE_COMPROMISE" - // UserKeyPhysicalCompromise - This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device. - UserKeyPhysicalCompromise AuthenticatorStatus = "USER_KEY_PHYSICAL_COMPROMISE" - // UpdateAvailable - A software or firmware update is available for the device. Additional data should be supplied including a URL where users can obtain an update and the date the update was published. - UpdateAvailable AuthenticatorStatus = "UPDATE_AVAILABLE" - // Revoked - The FIDO Alliance has determined that this authenticator should not be trusted for any reason, for example if it is known to be a fraudulent product or contain a deliberate backdoor. - Revoked AuthenticatorStatus = "REVOKED" - // SelfAssertionSubmitted - The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in StatusReport.url. - SelfAssertionSubmitted AuthenticatorStatus = "SELF_ASSERTION_SUBMITTED" - // FidoCertifiedL1 - The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED. - FidoCertifiedL1 AuthenticatorStatus = "FIDO_CERTIFIED_L1" - // FidoCertifiedL1plus - The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level 1. - FidoCertifiedL1plus AuthenticatorStatus = "FIDO_CERTIFIED_L1plus" - // FidoCertifiedL2 - The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level 1+. - FidoCertifiedL2 AuthenticatorStatus = "FIDO_CERTIFIED_L2" - // FidoCertifiedL2plus - The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level 2. - FidoCertifiedL2plus AuthenticatorStatus = "FIDO_CERTIFIED_L2plus" - // FidoCertifiedL3 - The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level 2+. - FidoCertifiedL3 AuthenticatorStatus = "FIDO_CERTIFIED_L3" - // FidoCertifiedL3plus - The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level 3. - FidoCertifiedL3plus AuthenticatorStatus = "FIDO_CERTIFIED_L3plus" -) +func (j StatusReportJSON) Parse() (report StatusReport, err error) { + var certificate *x509.Certificate -// UndesiredAuthenticatorStatus is an array of undesirable authenticator statuses -var UndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ - AttestationKeyCompromise, - UserVerificationBypass, - UserKeyRemoteCompromise, - UserKeyPhysicalCompromise, - Revoked, -} + if len(j.Certificate) != 0 { + if certificate, err = ParseMetadataX509Certificate(j.Certificate); err != nil { + return report, fmt.Errorf("error occurred parsing certificate value: %w", err) + } + } + + var effective time.Time -// IsUndesiredAuthenticatorStatus returns whether the supplied authenticator status is desirable or not -func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool { - for _, s := range UndesiredAuthenticatorStatus { - if s == status { - return true + if effective, err = time.Parse(time.DateOnly, j.EffectiveDate); err != nil { + return report, fmt.Errorf("error occurred parsing effective date value: %w", err) + } + + var uri *url.URL + + if len(j.URL) != 0 { + if uri, err = url.ParseRequestURI(j.URL); err != nil { + return report, fmt.Errorf("error occurred parsing URL value: %w", err) } } - return false + return StatusReport{ + Status: j.Status, + EffectiveDate: effective, + AuthenticatorVersion: j.AuthenticatorVersion, + Certificate: certificate, + URL: uri, + CertificationDescriptor: j.CertificationDescriptor, + CertificateNumber: j.CertificateNumber, + CertificationPolicyVersion: j.CertificationPolicyVersion, + CertificationRequirementsVersion: j.CertificationRequirementsVersion, + }, nil } -// RogueListEntry - Contains a list of individual authenticators known to be rogue +// RogueListEntry is a structure representing the RogueListEntry MDS3 dictionary. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#roguelistentry-dictionary type RogueListEntry struct { // Base64url encoding of the rogue authenticator's secret key Sk string `json:"sk"` + // ISO-8601 formatted date since when this entry is effective. Date string `json:"date"` } -// MetadataBLOBPayload - Represents the MetadataBLOBPayload -type MetadataBLOBPayload struct { - // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. - LegalHeader string `json:"legalHeader"` - // The serial number of this UAF Metadata TOC Payload. Serial numbers MUST be consecutive and strictly monotonic, i.e. the successor TOC will have a no value exactly incremented by one. - Number int `json:"no"` - // ISO-8601 formatted date when the next update will be provided at latest. - NextUpdate string `json:"nextUpdate"` - // List of zero or more MetadataTOCPayloadEntry objects. - Entries []MetadataBLOBPayloadEntry `json:"entries"` -} - -// CodeAccuracyDescriptor describes the relevant accuracy/complexity aspects of passcode user verification methods. +// CodeAccuracyDescriptor is a structure representing the CodeAccuracyDescriptor MDS3 dictionary. +// It describes the relevant accuracy/complexity aspects of passcode user verification methods. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#codeaccuracydescriptor-dictionary type CodeAccuracyDescriptor struct { // The numeric system base (radix) of the code, e.g. 10 in the case of decimal digits. Base uint16 `json:"base"` + // The minimum number of digits of the given base required for that code, e.g. 4 in the case of 4 digits. MinLength uint16 `json:"minLength"` + // Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block. MaxRetries uint16 `json:"maxRetries"` + // Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). // 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. // All alternative user verification methods MUST be specified appropriately in the Metadata in userVerificationDetails. BlockSlowdown uint16 `json:"blockSlowdown"` } -// The BiometricAccuracyDescriptor describes relevant accuracy/complexity aspects in the case of a biometric user verification method. +// BiometricAccuracyDescriptor is a structure representing the BiometricAccuracyDescriptor MDS3 dictionary. +// It describes relevant accuracy/complexity aspects in the case of a biometric user verification method. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#biometricaccuracydescriptor-dictionary type BiometricAccuracyDescriptor struct { // The false rejection rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with truthful claims of identity that are incorrectly denied. - SelfAttestedFRR int64 `json:"selfAttestedFRR "` + SelfAttestedFRR int64 `json:"selfAttestedFRR"` + // The false acceptance rate [ISO19795-1] for a single template, i.e. the percentage of verification transactions with wrongful claims of identity that are incorrectly confirmed. - SelfAttestedFAR int64 `json:"selfAttestedFAR "` + SelfAttestedFAR int64 `json:"selfAttestedFAR"` + // Maximum number of alternative templates from different fingers allowed. MaxTemplates uint16 `json:"maxTemplates"` + // Maximum number of false attempts before the authenticator will block this method (at least for some time). 0 means it will never block. MaxRetries uint16 `json:"maxRetries"` + // Enforced minimum number of seconds wait time after blocking (e.g. due to forced reboot or similar). // 0 means that this user verification method will be blocked either permanently or until an alternative user verification method succeeded. // All alternative user verification methods MUST be specified appropriately in the metadata in userVerificationDetails. BlockSlowdown uint16 `json:"blockSlowdown"` } -// The PatternAccuracyDescriptor describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method. +// PatternAccuracyDescriptor is a structure representing the PatternAccuracyDescriptor MDS3 dictionary. +// It describes relevant accuracy/complexity aspects in the case that a pattern is used as the user verification method. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#patternaccuracydescriptor-dictionary type PatternAccuracyDescriptor struct { // Number of possible patterns (having the minimum length) out of which exactly one would be the right one, i.e. 1/probability in the case of equal distribution. MinComplexity uint32 `json:"minComplexity"` + // Maximum number of false attempts before the authenticator will block authentication using this method (at least temporarily). 0 means it will never block. MaxRetries uint16 `json:"maxRetries"` + // Enforced minimum number of seconds wait time after blocking (due to forced reboot or similar mechanism). // 0 means this user verification method will be blocked, either permanently or until an alternative user verification method method succeeded. // All alternative user verification methods MUST be specified appropriately in the metadata under userVerificationDetails. BlockSlowdown uint16 `json:"blockSlowdown"` } -// VerificationMethodDescriptor - A descriptor for a specific base user verification method as implemented by the authenticator. +// VerificationMethodDescriptor is a structure representing the VerificationMethodDescriptor MDS3 dictionary. +// It describes a descriptor for a specific base user verification method as implemented by the authenticator. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#verificationmethoddescriptor-dictionary type VerificationMethodDescriptor struct { // a single USER_VERIFY constant (see [FIDORegistry]), not a bit flag combination. This value MUST be non-zero. UserVerificationMethod string `json:"userVerification"` + // May optionally be used in the case of method USER_VERIFY_PASSCODE. CaDesc CodeAccuracyDescriptor `json:"caDesc"` + // May optionally be used in the case of method USER_VERIFY_FINGERPRINT, USER_VERIFY_VOICEPRINT, USER_VERIFY_FACEPRINT, USER_VERIFY_EYEPRINT, or USER_VERIFY_HANDPRINT. BaDesc BiometricAccuracyDescriptor `json:"baDesc"` + // May optionally be used in case of method USER_VERIFY_PATTERN. PaDesc PatternAccuracyDescriptor `json:"paDesc"` } -// The rgbPaletteEntry is an RGB three-sample tuple palette entry -type rgbPaletteEntry struct { +// RGBPaletteEntry is a structure representing the RGBPaletteEntry MDS3 dictionary. +// It describes an RGB three-sample tuple palette entry. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#rgbpaletteentry-dictionary +type RGBPaletteEntry struct { // Red channel sample value R uint16 `json:"r"` + // Green channel sample value G uint16 `json:"g"` + // Blue channel sample value B uint16 `json:"b"` } -// The DisplayPNGCharacteristicsDescriptor describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table) +// DisplayPNGCharacteristicsDescriptor is a structure representing the DisplayPNGCharacteristicsDescriptor MDS3 dictionary. +// It describes a PNG image characteristics as defined in the PNG [PNG] spec for IHDR (image header) and PLTE (palette table)/ +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#displaypngcharacteristicsdescriptor-dictionary type DisplayPNGCharacteristicsDescriptor struct { // image width Width uint32 `json:"width"` + // image height Height uint32 `json:"height"` + // Bit depth - bits per sample or per palette index. BitDepth byte `json:"bitDepth"` + // Color type defines the PNG image type. ColorType byte `json:"colorType"` + // Compression method used to compress the image data. Compression byte `json:"compression"` + // Filter method is the preprocessing method applied to the image data before compression. Filter byte `json:"filter"` + // Interlace method is the transmission order of the image data. Interlace byte `json:"interlace"` + // 1 to 256 palette entries - Plte []rgbPaletteEntry `json:"plte"` + Plte []RGBPaletteEntry `json:"plte"` } -// EcdaaTrustAnchor - In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field. +// EcdaaTrustAnchor is a structure representing the EcdaaTrustAnchor MDS3 dictionary. +// In the case of ECDAA attestation, the ECDAA-Issuer's trust anchor MUST be specified in this field. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#ecdaatrustanchor-dictionary type EcdaaTrustAnchor struct { // base64url encoding of the result of ECPoint2ToB of the ECPoint2 X X string `json:"X"` + // base64url encoding of the result of ECPoint2ToB of the ECPoint2 Y Y string `json:"Y"` + // base64url encoding of the result of BigNumberToB(c) C string `json:"c"` + // base64url encoding of the result of BigNumberToB(sx) SX string `json:"sx"` + // base64url encoding of the result of BigNumberToB(sy) SY string `json:"sy"` + // Name of the Barreto-Naehrig elliptic curve for G1. "BN_P256", "BN_P638", "BN_ISOP256", and "BN_ISOP512" are supported. G1Curve string `json:"G1Curve"` } -// ExtensionDescriptor - This descriptor contains an extension supported by the authenticator. +// ExtensionDescriptor is a structure representing the ExtensionDescriptor MDS3 dictionary. +// This descriptor contains an extension supported by the authenticator. +// +// See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#extensiondescriptor-dictionary type ExtensionDescriptor struct { // Identifies the extension. ID string `json:"id"` + // The TAG of the extension if this was assigned. TAGs are assigned to extensions if they could appear in an assertion. Tag uint16 `json:"tag"` + // Contains arbitrary data further describing the extension and/or data needed to correctly process the extension. Data string `json:"data"` + // Indicates whether unknown extensions must be ignored (false) or must lead to an error (true) when the extension is to be processed by the FIDO Server, FIDO Client, ASM, or FIDO Authenticator. FailIfUnknown bool `json:"fail_if_unknown"` } -// MetadataStatement - Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information contained in the authoritative statement is used in several other places. -type MetadataStatement struct { - // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. - LegalHeader string `json:"legalHeader"` - // The Authenticator Attestation ID. - Aaid string `json:"aaid"` - // The Authenticator Attestation GUID. - AaGUID string `json:"aaguid"` - // A list of the attestation certificate public key identifiers encoded as hex string. - AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` - // A human-readable, short description of the authenticator, in English. - Description string `json:"description"` - // A list of human-readable short descriptions of the authenticator in different languages. - AlternativeDescriptions map[string]string `json:"alternativeDescriptions"` - // Earliest (i.e. lowest) trustworthy authenticatorVersion meeting the requirements specified in this metadata statement. - AuthenticatorVersion uint32 `json:"authenticatorVersion"` - // The FIDO protocol family. The values "uaf", "u2f", and "fido2" are supported. - ProtocolFamily string `json:"protocolFamily"` - // The FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator. - Upv []Version `json:"upv"` - // The list of authentication algorithms supported by the authenticator. - AuthenticationAlgorithms []AuthenticationAlgorithm `json:"authenticationAlgorithms"` - // The list of public key formats supported by the authenticator during registration operations. - PublicKeyAlgAndEncodings []PublicKeyAlgAndEncoding `json:"publicKeyAlgAndEncodings"` - // The supported attestation type(s). - AttestationTypes []AuthenticatorAttestationType `json:"attestationTypes"` - // A list of alternative VerificationMethodANDCombinations. - UserVerificationDetails [][]VerificationMethodDescriptor `json:"userVerificationDetails"` - // A 16-bit number representing the bit fields defined by the KEY_PROTECTION constants in the FIDO Registry of Predefined Values - KeyProtection []string `json:"keyProtection"` - // This entry is set to true or it is omitted, if the Uauth private key is restricted by the authenticator to only sign valid FIDO signature assertions. - // This entry is set to false, if the authenticator doesn't restrict the Uauth key to only sign valid FIDO signature assertions. - IsKeyRestricted bool `json:"isKeyRestricted"` - // This entry is set to true or it is omitted, if Uauth key usage always requires a fresh user verification - // This entry is set to false, if the Uauth key can be used without requiring a fresh user verification, e.g. without any additional user interaction, if the user was verified a (potentially configurable) caching time ago. - IsFreshUserVerificationRequired bool `json:"isFreshUserVerificationRequired"` - // A 16-bit number representing the bit fields defined by the MATCHER_PROTECTION constants in the FIDO Registry of Predefined Values - MatcherProtection []string `json:"matcherProtection"` - // The authenticator's overall claimed cryptographic strength in bits (sometimes also called security strength or security level). - CryptoStrength uint16 `json:"cryptoStrength"` - // A 32-bit number representing the bit fields defined by the ATTACHMENT_HINT constants in the FIDO Registry of Predefined Values - AttachmentHint []string `json:"attachmentHint"` - // A 16-bit number representing a combination of the bit flags defined by the TRANSACTION_CONFIRMATION_DISPLAY constants in the FIDO Registry of Predefined Values - TcDisplay []string `json:"tcDisplay"` - // Supported MIME content type [RFC2049] for the transaction confirmation display, such as text/plain or image/png. - TcDisplayContentType string `json:"tcDisplayContentType"` - // A list of alternative DisplayPNGCharacteristicsDescriptor. Each of these entries is one alternative of supported image characteristics for displaying a PNG image. - TcDisplayPNGCharacteristics []DisplayPNGCharacteristicsDescriptor `json:"tcDisplayPNGCharacteristics"` - // Each element of this array represents a PKIX [RFC5280] X.509 certificate that is a valid trust anchor for this authenticator model. - // Multiple certificates might be used for different batches of the same model. - // The array does not represent a certificate chain, but only the trust anchor of that chain. - // A trust anchor can be a root certificate, an intermediate CA certificate or even the attestation certificate itself. - AttestationRootCertificates []string `json:"attestationRootCertificates"` - // A list of trust anchors used for ECDAA attestation. This entry MUST be present if and only if attestationType includes ATTESTATION_ECDAA. - EcdaaTrustAnchors []EcdaaTrustAnchor `json:"ecdaaTrustAnchors"` - // A data: url [RFC2397] encoded PNG [PNG] icon for the Authenticator. - Icon string `json:"icon"` - // List of extensions supported by the authenticator. - SupportedExtensions []ExtensionDescriptor `json:"supportedExtensions"` - // Describes supported versions, extensions, AAGUID of the device and its capabilities - AuthenticatorGetInfo AuthenticatorGetInfo `json:"authenticatorGetInfo"` -} - -type AuthenticationAlgorithm string - -const ( - // An ECDSA signature on the NIST secp256r1 curve which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_raw" - // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the NIST secp256r1 curve. - ALG_SIGN_SECP256R1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_der" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. - ALG_SIGN_RSASSA_PSS_SHA256_RAW AuthenticationAlgorithm = "rsassa_pss_sha256_raw" - // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the RSASSA-PSS RFC3447 signature RFC4055 RFC4056. - ALG_SIGN_RSASSA_PSS_SHA256_DER AuthenticationAlgorithm = "rsassa_pss_sha256_der" - // An ECDSA signature on the secp256k1 curve which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_raw" - // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the secp256k1 curve. - ALG_SIGN_SECP256K1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_der" - // Chinese SM2 elliptic curve based signature algorithm combined with SM3 hash algorithm OSCCA-SM2 OSCCA-SM3. - ALG_SIGN_SM2_SM3_RAW AuthenticationAlgorithm = "sm2_sm3_raw" - // This is the EMSA-PKCS1-v1_5 signature as defined in RFC3447. - ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_raw" - // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the EMSA-PKCS1-v1_5 signature as defined in RFC3447. - ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_der" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. - ALG_SIGN_RSASSA_PSS_SHA384_RAW AuthenticationAlgorithm = "rsassa_pss_sha384_raw" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. - ALG_SIGN_RSASSA_PSS_SHA512_RAW AuthenticationAlgorithm = "rsassa_pss_sha512_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA256(aka RS256) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 - ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha256_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA384(aka RS384) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 - ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha384_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA512(aka RS512) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 - ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha512_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA1(aka RS1) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 - ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha1_raw" - // An ECDSA signature on the NIST secp384r1 curve with SHA384(aka: ES384) which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW AuthenticationAlgorithm = "secp384r1_ecdsa_sha384_raw" - // An ECDSA signature on the NIST secp512r1 curve with SHA512(aka: ES512) which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW AuthenticationAlgorithm = "secp521r1_ecdsa_sha512_raw" - // An EdDSA signature on the curve 25519, which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_ED25519_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed25519_eddsa_sha512_raw" - // An EdDSA signature on the curve Ed448, which must have raw R and S buffers, encoded in big-endian order. - ALG_SIGN_ED448_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed448_eddsa_sha512_raw" -) - -// TODO: this goes away after webauthncose.CredentialPublicKey gets implemented -type algKeyCose struct { - KeyType webauthncose.COSEKeyType - Algorithm webauthncose.COSEAlgorithmIdentifier - Curve webauthncose.COSEEllipticCurve -} - -func algKeyCoseDictionary() func(AuthenticationAlgorithm) algKeyCose { - mapping := map[AuthenticationAlgorithm]algKeyCose{ - ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256}, - ALG_SIGN_SECP256R1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256}, - ALG_SIGN_RSASSA_PSS_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256}, - ALG_SIGN_RSASSA_PSS_SHA256_DER: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256}, - ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1}, - ALG_SIGN_SECP256K1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1}, - ALG_SIGN_RSASSA_PSS_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS384}, - ALG_SIGN_RSASSA_PSS_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS512}, - ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS256}, - ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS384}, - ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS512}, - ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS1}, - ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES384, Curve: webauthncose.P384}, - ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES512, Curve: webauthncose.P521}, - ALG_SIGN_ED25519_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed25519}, - ALG_SIGN_ED448_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed448}, - } - - return func(key AuthenticationAlgorithm) algKeyCose { - return mapping[key] - } -} - -func AlgKeyMatch(key algKeyCose, algs []AuthenticationAlgorithm) bool { - for _, alg := range algs { - if reflect.DeepEqual(algKeyCoseDictionary()(alg), key) { - return true - } - } - - return false -} - -type PublicKeyAlgAndEncoding string - -const ( - // Raw ANSI X9.62 formatted Elliptic Curve public key. - ALG_KEY_ECC_X962_RAW PublicKeyAlgAndEncoding = "ecc_x962_raw" - // DER ITU-X690-2008 encoded ANSI X.9.62 formatted SubjectPublicKeyInfo RFC5480 specifying an elliptic curve public key. - ALG_KEY_ECC_X962_DER PublicKeyAlgAndEncoding = "ecc_x962_der" - // Raw encoded 2048-bit RSA public key RFC3447. - ALG_KEY_RSA_2048_RAW PublicKeyAlgAndEncoding = "rsa_2048_raw" - // ASN.1 DER [ITU-X690-2008] encoded 2048-bit RSA RFC3447 public key RFC4055. - ALG_KEY_RSA_2048_DER PublicKeyAlgAndEncoding = "rsa_2048_der" - // COSE_Key format, as defined in Section 7 of RFC8152. This encoding includes its own field for indicating the public key algorithm. - ALG_KEY_COSE PublicKeyAlgAndEncoding = "cose" -) - -// Version - Represents a generic version with major and minor fields. +// Version represents a generic version with major and minor fields. type Version struct { // Major version. Major uint16 `json:"major"` + // Minor version. Minor uint16 `json:"minor"` } type AuthenticatorGetInfo struct { // List of supported versions. - Versions []string `json:"versions"` + Versions []string + // List of supported extensions. - Extensions []string `json:"extensions"` + Extensions []string + // The claimed AAGUID. - AaGUID string `json:"aaguid"` + AaGUID uuid.UUID + // List of supported options. - Options map[string]bool `json:"options"` + Options map[string]bool + // Maximum message size supported by the authenticator. - MaxMsgSize uint `json:"maxMsgSize"` + MaxMsgSize uint + // List of supported PIN/UV auth protocols in order of decreasing authenticator preference. - PivUvAuthProtocols []uint `json:"pinUvAuthProtocols"` + PivUvAuthProtocols []uint + // Maximum number of credentials supported in credentialID list at a time by the authenticator. - MaxCredentialCountInList uint `json:"maxCredentialCountInList"` + MaxCredentialCountInList uint + // Maximum Credential ID Length supported by the authenticator. - MaxCredentialIdLength uint `json:"maxCredentialLength"` + MaxCredentialIdLength uint + // List of supported transports. - Transports []string `json:"transports"` + Transports []string + // List of supported algorithms for credential generation, as specified in WebAuthn. - Algorithms []PublicKeyCredentialParameters `json:"algorithms"` + Algorithms []PublicKeyCredentialParameters + // The maximum size, in bytes, of the serialized large-blob array that this authenticator can store. - MaxSerializedLargeBlobArray uint `json:"maxSerializedLargeBlobArray"` + MaxSerializedLargeBlobArray uint + // If this member is present and set to true, the PIN must be changed. - ForcePINChange bool `json:"forcePINChange"` + ForcePINChange bool + // This specifies the current minimum PIN length, in Unicode code points, the authenticator enforces for ClientPIN. - MinPINLength uint `json:"minPINLength"` + MinPINLength uint + // Indicates the firmware version of the authenticator model identified by AAGUID. - FirmwareVersion uint `json:"firmwareVersion"` + FirmwareVersion uint + // Maximum credBlob length in bytes supported by the authenticator. - MaxCredBlobLength uint `json:"maxCredBlobLength"` + MaxCredBlobLength uint + // This specifies the max number of RP IDs that authenticator can set via setMinPINLength subcommand. - MaxRPIDsForSetMinPINLength uint `json:"maxRPIDsForSetMinPINLength"` + MaxRPIDsForSetMinPINLength uint + // This specifies the preferred number of invocations of the getPinUvAuthTokenUsingUvWithPermissions subCommand the platform may attempt before falling back to the getPinUvAuthTokenUsingPinWithPermissions subCommand or displaying an error. - PreferredPlatformUvAttempts uint `json:"preferredPlatformUvAttempts"` + PreferredPlatformUvAttempts uint + // This specifies the user verification modality supported by the authenticator via authenticatorClientPIN's getPinUvAuthTokenUsingUvWithPermissions subcommand. - UvModality uint `json:"uvModality"` + UvModality uint + // This specifies a list of authenticator certifications. - Certifications map[string]float64 `json:"certifications"` + Certifications map[string]float64 + // If this member is present it indicates the estimated number of additional discoverable credentials that can be stored. - RemainingDiscoverableCredentials uint `json:"remainingDiscoverableCredentials"` + RemainingDiscoverableCredentials uint + // If present the authenticator supports the authenticatorConfig vendorPrototype subcommand, and its value is a list of authenticatorConfig vendorCommandId values supported, which MAY be empty. - VendorPrototypeConfigCommands []uint `json:"vendorPrototypeConfigCommands"` + VendorPrototypeConfigCommands []uint +} + +type AuthenticatorGetInfoJSON struct { + Versions []string `json:"versions"` + Extensions []string `json:"extensions"` + AaGUID string `json:"aaguid"` + Options map[string]bool `json:"options"` + MaxMsgSize uint `json:"maxMsgSize"` + PivUvAuthProtocols []uint `json:"pinUvAuthProtocols"` + MaxCredentialCountInList uint `json:"maxCredentialCountInList"` + MaxCredentialIdLength uint `json:"maxCredentialLength"` + Transports []string `json:"transports"` + Algorithms []PublicKeyCredentialParameters `json:"algorithms"` + MaxSerializedLargeBlobArray uint `json:"maxSerializedLargeBlobArray"` + ForcePINChange bool `json:"forcePINChange"` + MinPINLength uint `json:"minPINLength"` + FirmwareVersion uint `json:"firmwareVersion"` + MaxCredBlobLength uint `json:"maxCredBlobLength"` + MaxRPIDsForSetMinPINLength uint `json:"maxRPIDsForSetMinPINLength"` + PreferredPlatformUvAttempts uint `json:"preferredPlatformUvAttempts"` + UvModality uint `json:"uvModality"` + Certifications map[string]float64 `json:"certifications"` + RemainingDiscoverableCredentials uint `json:"remainingDiscoverableCredentials"` + VendorPrototypeConfigCommands []uint `json:"vendorPrototypeConfigCommands"` +} + +func (j AuthenticatorGetInfoJSON) Parse() (info AuthenticatorGetInfo, err error) { + var aaguid uuid.UUID + + if len(j.AaGUID) != 0 { + if aaguid, err = uuid.Parse(j.AaGUID); err != nil { + return info, fmt.Errorf("error occurred parsing AAGUID value: %w", err) + } + } + + return AuthenticatorGetInfo{ + Versions: j.Versions, + Extensions: j.Extensions, + AaGUID: aaguid, + Options: j.Options, + MaxMsgSize: j.MaxMsgSize, + PivUvAuthProtocols: j.PivUvAuthProtocols, + MaxCredentialCountInList: j.MaxCredentialCountInList, + MaxCredentialIdLength: j.MaxCredentialIdLength, + Transports: j.Transports, + Algorithms: j.Algorithms, + MaxSerializedLargeBlobArray: j.MaxSerializedLargeBlobArray, + ForcePINChange: j.ForcePINChange, + MinPINLength: j.MinPINLength, + FirmwareVersion: j.FirmwareVersion, + MaxCredBlobLength: j.MaxCredBlobLength, + MaxRPIDsForSetMinPINLength: j.MaxRPIDsForSetMinPINLength, + PreferredPlatformUvAttempts: j.PreferredPlatformUvAttempts, + UvModality: j.UvModality, + Certifications: j.Certifications, + RemainingDiscoverableCredentials: j.RemainingDiscoverableCredentials, + VendorPrototypeConfigCommands: j.VendorPrototypeConfigCommands, + }, nil } // MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint. @@ -541,189 +840,7 @@ type MDSGetEndpointsRequest struct { type MDSGetEndpointsResponse struct { // The status of the response. Status string `json:"status"` + // An array of urls, each pointing to a MetadataTOCPayload. Result []string `json:"result"` } - -func unmarshalMDSBLOB(body []byte, c http.Client) (MetadataBLOBPayload, error) { - var payload MetadataBLOBPayload - - token, err := jwt.Parse(string(body), func(token *jwt.Token) (any, error) { - // 2. If the x5u attribute is present in the JWT Header, then - if _, ok := token.Header["x5u"].(any); ok { - // never seen an x5u here, although it is in the spec - return nil, errors.New("x5u encountered in header of metadata TOC payload") - } - var chain []any - // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. - - if x5c, ok := token.Header["x5c"].([]any); !ok { - // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. - chain[0] = MDSRoot - } else { - chain = x5c - } - - // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. - valid, err := validateChain(chain, c) - if !valid || err != nil { - return nil, err - } - - // Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the - // certificate bytes. - o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) - - // base64 decode the certificate into the buffer. - n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string))) - if err != nil { - return nil, err - } - - // Parse the certificate from the buffer. - cert, err := x509.ParseCertificate(o[:n]) - if err != nil { - return nil, err - } - - // 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain - // jwt.Parse() uses the TOC signing certificate public key internally to verify the signature. - return cert.PublicKey, err - }) - - if err != nil { - return payload, err - } - - err = mapstructure.Decode(token.Claims, &payload) - - return payload, err -} - -func validateChain(chain []any, c http.Client) (bool, error) { - oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(MDSRoot))) - - nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(MDSRoot)) - if err != nil { - return false, err - } - - rootcert, err := x509.ParseCertificate(oRoot[:nRoot]) - if err != nil { - return false, err - } - - roots := x509.NewCertPool() - - roots.AddCert(rootcert) - - o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[1].(string)))) - - n, err := base64.StdEncoding.Decode(o, []byte(chain[1].(string))) - if err != nil { - return false, err - } - - intcert, err := x509.ParseCertificate(o[:n]) - if err != nil { - return false, err - } - - if revoked, ok := revoke.VerifyCertificate(intcert); !ok { - issuer := intcert.IssuingCertificateURL - - if issuer != nil { - return false, errCRLUnavailable - } - } else if revoked { - return false, errIntermediateCertRevoked - } - - ints := x509.NewCertPool() - ints.AddCert(intcert) - - l := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) - - n, err = base64.StdEncoding.Decode(l, []byte(chain[0].(string))) - if err != nil { - return false, err - } - - leafcert, err := x509.ParseCertificate(l[:n]) - if err != nil { - return false, err - } - - if revoked, ok := revoke.VerifyCertificate(leafcert); !ok { - return false, errCRLUnavailable - } else if revoked { - return false, errLeafCertRevoked - } - - opts := x509.VerifyOptions{ - Roots: roots, - Intermediates: ints, - } - - _, err = leafcert.Verify(opts) - - return err == nil, err -} - -type MetadataError struct { - // Short name for the type of error that has occurred. - Type string `json:"type"` - // Additional details about the error. - Details string `json:"error"` - // Information to help debug the error. - DevInfo string `json:"debug"` -} - -var ( - errIntermediateCertRevoked = &MetadataError{ - Type: "intermediate_revoked", - Details: "Intermediate certificate is on issuers revocation list", - } - errLeafCertRevoked = &MetadataError{ - Type: "leaf_revoked", - Details: "Leaf certificate is on issuers revocation list", - } - errCRLUnavailable = &MetadataError{ - Type: "crl_unavailable", - Details: "Certificate revocation list is unavailable", - } -) - -func (err *MetadataError) Error() string { - return err.Details -} - -func PopulateMetadata(url string) error { - c := &http.Client{ - Timeout: time.Second * 30, - } - - res, err := c.Get(url) - if err != nil { - return err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return err - } - - blob, err := unmarshalMDSBLOB(body, *c) - if err != nil { - return err - } - - for _, entry := range blob.Entries { - aaguid, _ := uuid.Parse(entry.AaGUID) - Metadata[aaguid] = entry - } - - return err -} diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index acb3381..8210b7a 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -9,91 +9,13 @@ import ( "time" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/go-webauthn/webauthn/protocol/webauthncose" ) -func downloadBytes(url string, c http.Client) ([]byte, error) { - res, err := c.Get(url) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - body, _ := io.ReadAll(res.Body) - - return body, err -} - -func getEndpoints(c http.Client) ([]string, error) { - jsonReq, err := json.Marshal(MDSGetEndpointsRequest{Endpoint: "https://webauthn.io"}) - if err != nil { - return nil, err - } - - req, err := c.Post("https://mds3.fido.tools/getEndpoints", "application/json", bytes.NewBuffer(jsonReq)) - if err != nil { - return nil, err - } - - defer req.Body.Close() - body, _ := io.ReadAll(req.Body) - - var resp MDSGetEndpointsResponse - - if err = json.Unmarshal(body, &resp); err != nil { - return nil, err - } - - return resp.Result, err -} - -func getTestMetadata(s string, c http.Client) (MetadataStatement, error) { - var statement MetadataStatement - - // MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint. - type MDSGetTestMetadata struct { - // The URL of the local server endpoint, e.g. https://webauthn.io/ - Endpoint string `json:"endpoint"` - TestCase string `json:"testcase"` - } - - jsonReq, err := json.Marshal(MDSGetTestMetadata{Endpoint: "https://webauthn.io", TestCase: s}) - if err != nil { - return statement, err - } - - req, err := c.Post("https://mds3.fido.tools/getTestMetadata", "application/json", bytes.NewBuffer(jsonReq)) - if err != nil { - return statement, err - } - - defer req.Body.Close() - - body, err := io.ReadAll(req.Body) - if err != nil { - return statement, err - } - - type ConformanceResponse struct { - Status string `json:"status"` - Result MetadataStatement `json:"result"` - } - - var resp ConformanceResponse - - if err = json.Unmarshal(body, &resp); err != nil { - return statement, err - } - - statement = resp.Result - - return statement, err -} - func TestProductionMetadataTOCParsing(t *testing.T) { - if err := PopulateMetadata(ProductionMDSURL); err != nil { + if err := PopulateMetadata(ProductionMDSURL, true); err != nil { t.Fatal(err) } } @@ -105,7 +27,7 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { Timeout: time.Second * 30, } - tests := []struct { + testCases := []struct { name string pass bool }{ @@ -135,49 +57,58 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { }, } - endpoints, err := getEndpoints(*httpClient) + endpoints, err := getEndpoints(httpClient) if err != nil { t.Fatal(err) } + decoder := NewDecoder() + + metadata := make(map[uuid.UUID]MetadataBLOBPayloadEntryJSON) + for _, endpoint := range endpoints { - bytes, err := downloadBytes(endpoint, *httpClient) + bytes, err := downloadBytes(endpoint, httpClient) if err != nil { t.Fatal(err) } - blob, err := unmarshalMDSBLOB(bytes, *httpClient) + blob, err := decoder.Decode(bytes) if err != nil { if me, ok := err.(*MetadataError); ok { t.Log(me.Details) } } - for _, entry := range blob.Entries { - aaguid, _ := uuid.Parse(entry.AaGUID) - Metadata[aaguid] = entry + if blob != nil { + for _, entry := range blob.Entries { + aaguid, _ := uuid.Parse(entry.AaGUID) + metadata[aaguid] = entry + } } } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - statement, err := getTestMetadata(tt.name, *httpClient) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + statement, err := getTestMetadata(tc.name, httpClient) if err != nil { t.Fatal(err) } aaguid, _ := uuid.Parse(statement.AaGUID) - if meta, ok := Metadata[aaguid]; ok { - if tt.pass { - t.Logf("Found aaguid %s in test metadata", meta.AaGUID) - } else { - if IsUndesiredAuthenticatorStatus(meta.StatusReports[0].Status) { - t.Logf("Found authenticator %s with bad status in test metadata, %s", meta.AaGUID, meta.StatusReports[0].Status) - } else { - t.Fail() + if meta, ok := metadata[aaguid]; ok { + pass := true + + for _, report := range meta.StatusReports { + if IsUndesiredAuthenticatorStatus(report.Status) { + pass = false } } + + assert.Equal(t, tc.pass, pass, "One or more status reports had an undesired status but this was not expected.") + + _, err := meta.Parse() + assert.NoError(t, err, "Failed to parse metadata") } else { - if !tt.pass { + if !tc.pass { t.Logf("Metadata for aaguid %s not found in test metadata", statement.AaGUID) } else { t.Fail() @@ -194,13 +125,11 @@ const ( func TestExampleMetadataTOCParsing(t *testing.T) { MDSRoot = ExampleMDSRoot - httpClient := &http.Client{ - Timeout: time.Second * 30, - } - exampleMetadataBLOBBytes := bytes.NewBufferString(exampleMetadataBLOB) - _, err := unmarshalMDSBLOB(exampleMetadataBLOBBytes.Bytes(), *httpClient) + decoder := NewDecoder() + + _, err := decoder.Decode(exampleMetadataBLOBBytes.Bytes()) if err != nil { t.Fail() } @@ -347,3 +276,82 @@ func TestAlgKeyMatch(t *testing.T) { }) } } + +func downloadBytes(url string, c *http.Client) ([]byte, error) { + res, err := c.Get(url) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + body, _ := io.ReadAll(res.Body) + + return body, err +} + +func getEndpoints(c *http.Client) ([]string, error) { + jsonReq, err := json.Marshal(MDSGetEndpointsRequest{Endpoint: "https://webauthn.io"}) + if err != nil { + return nil, err + } + + req, err := c.Post("https://mds3.fido.tools/getEndpoints", "application/json", bytes.NewBuffer(jsonReq)) + if err != nil { + return nil, err + } + + defer req.Body.Close() + body, _ := io.ReadAll(req.Body) + + var resp MDSGetEndpointsResponse + + if err = json.Unmarshal(body, &resp); err != nil { + return nil, err + } + + return resp.Result, err +} + +func getTestMetadata(s string, c *http.Client) (MetadataStatementJSON, error) { + var statement MetadataStatementJSON + + // MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint. + type MDSGetTestMetadata struct { + // The URL of the local server endpoint, e.g. https://webauthn.io/ + Endpoint string `json:"endpoint"` + TestCase string `json:"testcase"` + } + + jsonReq, err := json.Marshal(MDSGetTestMetadata{Endpoint: "https://webauthn.io", TestCase: s}) + if err != nil { + return statement, err + } + + req, err := c.Post("https://mds3.fido.tools/getTestMetadata", "application/json", bytes.NewBuffer(jsonReq)) + if err != nil { + return statement, err + } + + defer req.Body.Close() + + body, err := io.ReadAll(req.Body) + if err != nil { + return statement, err + } + + type ConformanceResponse struct { + Status string `json:"status"` + Result MetadataStatementJSON `json:"result"` + } + + var resp ConformanceResponse + + if err = json.Unmarshal(body, &resp); err != nil { + return statement, err + } + + statement = resp.Result + + return statement, err +} diff --git a/metadata/types.go b/metadata/types.go new file mode 100644 index 0000000..d74a609 --- /dev/null +++ b/metadata/types.go @@ -0,0 +1,267 @@ +package metadata + +import ( + "context" + "fmt" + "io" + "net/http" + "reflect" + "time" + + "github.com/google/uuid" + + "github.com/go-webauthn/webauthn/protocol/webauthncose" +) + +type Provider interface { + GetMetadataBLOBPayloadEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) +} + +type PublicKeyCredentialParameters struct { + Type string `json:"type"` + Alg webauthncose.COSEAlgorithmIdentifier `json:"alg"` +} + +// AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports. +// Each constant has a case-sensitive string representation (in quotes), which is used in the authoritative metadata for FIDO authenticators. +type AuthenticatorAttestationType string + +const ( + // BasicFull - Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). Authenticators must provide its attestation signature during the registration process for the same reason. The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). This sharing process should be done according to [UAFMetadataService]. + BasicFull AuthenticatorAttestationType = "basic_full" + // BasicSurrogate - Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. As a consequence it does not provide a cryptographic proof of the security characteristics. But it is the best thing we can do if the authenticator is not able to have an attestation private key. + BasicSurrogate AuthenticatorAttestationType = "basic_surrogate" + // Ecdaa - Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. + Ecdaa AuthenticatorAttestationType = "ecdaa" + // AttCA - Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. + AttCA AuthenticatorAttestationType = "attca" + // AnonCA In this case, the authenticator uses an Anonymization CA which dynamically generates per-credential attestation certificates such that the attestation statements presented to Relying Parties do not provide uniquely identifiable information, e.g., that might be used for tracking purposes. The applicable [WebAuthn] attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application Attestation "apple-appattest". + AnonCA AuthenticatorAttestationType = "anonca" + // None - Indicates absence of attestation + None AuthenticatorAttestationType = "none" +) + +// AuthenticatorStatus - This enumeration describes the status of an authenticator model as identified by its AAID and potentially some additional information (such as a specific attestation key). +// https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#authenticatorstatus-enum +type AuthenticatorStatus string + +const ( + // NotFidoCertified - This authenticator is not FIDO certified. + NotFidoCertified AuthenticatorStatus = "NOT_FIDO_CERTIFIED" + // FidoCertified - This authenticator has passed FIDO functional certification. This certification scheme is phased out and will be replaced by FIDO_CERTIFIED_L1. + FidoCertified AuthenticatorStatus = "FIDO_CERTIFIED" + // UserVerificationBypass - Indicates that malware is able to bypass the user verification. This means that the authenticator could be used without the user's consent and potentially even without the user's knowledge. + UserVerificationBypass AuthenticatorStatus = "USER_VERIFICATION_BYPASS" + // AttestationKeyCompromise - Indicates that an attestation key for this authenticator is known to be compromised. Additional data should be supplied, including the key identifier and the date of compromise, if known. + AttestationKeyCompromise AuthenticatorStatus = "ATTESTATION_KEY_COMPROMISE" + // UserKeyRemoteCompromise - This authenticator has identified weaknesses that allow registered keys to be compromised and should not be trusted. This would include both, e.g. weak entropy that causes predictable keys to be generated or side channels that allow keys or signatures to be forged, guessed or extracted. + UserKeyRemoteCompromise AuthenticatorStatus = "USER_KEY_REMOTE_COMPROMISE" + // UserKeyPhysicalCompromise - This authenticator has known weaknesses in its key protection mechanism(s) that allow user keys to be extracted by an adversary in physical possession of the device. + UserKeyPhysicalCompromise AuthenticatorStatus = "USER_KEY_PHYSICAL_COMPROMISE" + // UpdateAvailable - A software or firmware update is available for the device. Additional data should be supplied including a URL where users can obtain an update and the date the update was published. + UpdateAvailable AuthenticatorStatus = "UPDATE_AVAILABLE" + // Revoked - The FIDO Alliance has determined that this authenticator should not be trusted for any reason, for example if it is known to be a fraudulent product or contain a deliberate backdoor. + Revoked AuthenticatorStatus = "REVOKED" + // SelfAssertionSubmitted - The authenticator vendor has completed and submitted the self-certification checklist to the FIDO Alliance. If this completed checklist is publicly available, the URL will be specified in StatusReportJSON.url. + SelfAssertionSubmitted AuthenticatorStatus = "SELF_ASSERTION_SUBMITTED" + // FidoCertifiedL1 - The authenticator has passed FIDO Authenticator certification at level 1. This level is the more strict successor of FIDO_CERTIFIED. + FidoCertifiedL1 AuthenticatorStatus = "FIDO_CERTIFIED_L1" + // FidoCertifiedL1plus - The authenticator has passed FIDO Authenticator certification at level 1+. This level is the more than level 1. + FidoCertifiedL1plus AuthenticatorStatus = "FIDO_CERTIFIED_L1plus" + // FidoCertifiedL2 - The authenticator has passed FIDO Authenticator certification at level 2. This level is more strict than level 1+. + FidoCertifiedL2 AuthenticatorStatus = "FIDO_CERTIFIED_L2" + // FidoCertifiedL2plus - The authenticator has passed FIDO Authenticator certification at level 2+. This level is more strict than level 2. + FidoCertifiedL2plus AuthenticatorStatus = "FIDO_CERTIFIED_L2plus" + // FidoCertifiedL3 - The authenticator has passed FIDO Authenticator certification at level 3. This level is more strict than level 2+. + FidoCertifiedL3 AuthenticatorStatus = "FIDO_CERTIFIED_L3" + // FidoCertifiedL3plus - The authenticator has passed FIDO Authenticator certification at level 3+. This level is more strict than level 3. + FidoCertifiedL3plus AuthenticatorStatus = "FIDO_CERTIFIED_L3plus" +) + +// UndesiredAuthenticatorStatus is an array of undesirable authenticator statuses +var UndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ + AttestationKeyCompromise, + UserVerificationBypass, + UserKeyRemoteCompromise, + UserKeyPhysicalCompromise, + Revoked, +} + +// IsUndesiredAuthenticatorStatus returns whether the supplied authenticator status is desirable or not +func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool { + for _, s := range UndesiredAuthenticatorStatus { + if s == status { + return true + } + } + + return false +} + +type AuthenticationAlgorithm string + +const ( + // An ECDSA signature on the NIST secp256r1 curve which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_raw" + // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the NIST secp256r1 curve. + ALG_SIGN_SECP256R1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_der" + // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + ALG_SIGN_RSASSA_PSS_SHA256_RAW AuthenticationAlgorithm = "rsassa_pss_sha256_raw" + // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the RSASSA-PSS RFC3447 signature RFC4055 RFC4056. + ALG_SIGN_RSASSA_PSS_SHA256_DER AuthenticationAlgorithm = "rsassa_pss_sha256_der" + // An ECDSA signature on the secp256k1 curve which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_raw" + // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the secp256k1 curve. + ALG_SIGN_SECP256K1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_der" + // Chinese SM2 elliptic curve based signature algorithm combined with SM3 hash algorithm OSCCA-SM2 OSCCA-SM3. + ALG_SIGN_SM2_SM3_RAW AuthenticationAlgorithm = "sm2_sm3_raw" + // This is the EMSA-PKCS1-v1_5 signature as defined in RFC3447. + ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_raw" + // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the EMSA-PKCS1-v1_5 signature as defined in RFC3447. + ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_der" + // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + ALG_SIGN_RSASSA_PSS_SHA384_RAW AuthenticationAlgorithm = "rsassa_pss_sha384_raw" + // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + ALG_SIGN_RSASSA_PSS_SHA512_RAW AuthenticationAlgorithm = "rsassa_pss_sha512_raw" + // RSASSA-PKCS1-v1_5 RFC3447 with SHA256(aka RS256) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha256_raw" + // RSASSA-PKCS1-v1_5 RFC3447 with SHA384(aka RS384) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha384_raw" + // RSASSA-PKCS1-v1_5 RFC3447 with SHA512(aka RS512) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha512_raw" + // RSASSA-PKCS1-v1_5 RFC3447 with SHA1(aka RS1) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha1_raw" + // An ECDSA signature on the NIST secp384r1 curve with SHA384(aka: ES384) which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW AuthenticationAlgorithm = "secp384r1_ecdsa_sha384_raw" + // An ECDSA signature on the NIST secp512r1 curve with SHA512(aka: ES512) which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW AuthenticationAlgorithm = "secp521r1_ecdsa_sha512_raw" + // An EdDSA signature on the curve 25519, which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_ED25519_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed25519_eddsa_sha512_raw" + // An EdDSA signature on the curve Ed448, which must have raw R and S buffers, encoded in big-endian order. + ALG_SIGN_ED448_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed448_eddsa_sha512_raw" +) + +// TODO: this goes away after webauthncose.CredentialPublicKey gets implemented +type algKeyCose struct { + KeyType webauthncose.COSEKeyType + Algorithm webauthncose.COSEAlgorithmIdentifier + Curve webauthncose.COSEEllipticCurve +} + +func algKeyCoseDictionary() func(AuthenticationAlgorithm) algKeyCose { + mapping := map[AuthenticationAlgorithm]algKeyCose{ + ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256}, + ALG_SIGN_SECP256R1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256, Curve: webauthncose.P256}, + ALG_SIGN_RSASSA_PSS_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256}, + ALG_SIGN_RSASSA_PSS_SHA256_DER: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS256}, + ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1}, + ALG_SIGN_SECP256K1_ECDSA_SHA256_DER: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES256K, Curve: webauthncose.Secp256k1}, + ALG_SIGN_RSASSA_PSS_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS384}, + ALG_SIGN_RSASSA_PSS_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgPS512}, + ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS256}, + ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS384}, + ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS512}, + ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW: {KeyType: webauthncose.RSAKey, Algorithm: webauthncose.AlgRS1}, + ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES384, Curve: webauthncose.P384}, + ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW: {KeyType: webauthncose.EllipticKey, Algorithm: webauthncose.AlgES512, Curve: webauthncose.P521}, + ALG_SIGN_ED25519_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed25519}, + ALG_SIGN_ED448_EDDSA_SHA512_RAW: {KeyType: webauthncose.OctetKey, Algorithm: webauthncose.AlgEdDSA, Curve: webauthncose.Ed448}, + } + + return func(key AuthenticationAlgorithm) algKeyCose { + return mapping[key] + } +} + +func AlgKeyMatch(key algKeyCose, algs []AuthenticationAlgorithm) bool { + for _, alg := range algs { + if reflect.DeepEqual(algKeyCoseDictionary()(alg), key) { + return true + } + } + + return false +} + +type PublicKeyAlgAndEncoding string + +const ( + // Raw ANSI X9.62 formatted Elliptic Curve public key. + ALG_KEY_ECC_X962_RAW PublicKeyAlgAndEncoding = "ecc_x962_raw" + // DER ITU-X690-2008 encoded ANSI X.9.62 formatted SubjectPublicKeyInfo RFC5480 specifying an elliptic curve public key. + ALG_KEY_ECC_X962_DER PublicKeyAlgAndEncoding = "ecc_x962_der" + // Raw encoded 2048-bit RSA public key RFC3447. + ALG_KEY_RSA_2048_RAW PublicKeyAlgAndEncoding = "rsa_2048_raw" + // ASN.1 DER [ITU-X690-2008] encoded 2048-bit RSA RFC3447 public key RFC4055. + ALG_KEY_RSA_2048_DER PublicKeyAlgAndEncoding = "rsa_2048_der" + // COSE_Key format, as defined in Section 7 of RFC8152. This encoding includes its own field for indicating the public key algorithm. + ALG_KEY_COSE PublicKeyAlgAndEncoding = "cose" +) + +type MetadataError struct { + // Short name for the type of error that has occurred. + Type string `json:"type"` + // Additional details about the error. + Details string `json:"error"` + // Information to help debug the error. + DevInfo string `json:"debug"` +} + +var ( + errIntermediateCertRevoked = &MetadataError{ + Type: "intermediate_revoked", + Details: "Intermediate certificate is on issuers revocation list", + } + errLeafCertRevoked = &MetadataError{ + Type: "leaf_revoked", + Details: "Leaf certificate is on issuers revocation list", + } + errCRLUnavailable = &MetadataError{ + Type: "crl_unavailable", + Details: "Certificate revocation list is unavailable", + } +) + +func (err *MetadataError) Error() string { + return err.Details +} + +func PopulateMetadata(url string, skipInvalid bool) error { + c := &http.Client{ + Timeout: time.Second * 30, + } + + res, err := c.Get(url) + if err != nil { + return err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return err + } + + blob, err := unmarshalMDSBLOB(body, c) + if err != nil { + return err + } + + for _, entry := range blob.Entries { + fmt.Println("parsing", entry.AaGUID, entry.MetadataStatement.Description) + + parsed, err := entry.Parse() + if err != nil { + if skipInvalid { + continue + } + + return err + } + + Metadata[parsed.AaGUID] = parsed + } + + return err +} diff --git a/protocol/attestation.go b/protocol/attestation.go index 36a264a..5ad5f8e 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -175,8 +175,9 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client } if x5c != nil { - x5cAtt, err := x509.ParseCertificate(x5c[0].([]byte)) - if err != nil { + var x5cAtt *x509.Certificate + + if x5cAtt, err = x509.ParseCertificate(x5c[0].([]byte)); err != nil { return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") } @@ -192,6 +193,10 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client if !hasBasicFull { return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") } + + if _, err = x5cAtt.Verify(meta.MetadataStatement.Verifier()); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) + } } } } else if metadata.Conformance { diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index 15831c4..786009f 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -6,13 +6,13 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/go-webauthn/webauthn/metadata" ) func TestAttestationVerify(t *testing.T) { - if err := metadata.PopulateMetadata(metadata.ProductionMDSURL); err != nil { - t.Fatal(err) - } + assert.NoError(t, metadata.PopulateMetadata(metadata.ProductionMDSURL, true)) for i := range testAttestationOptions { t.Run(fmt.Sprintf("Running test %d", i), func(t *testing.T) { @@ -44,6 +44,8 @@ func TestAttestationVerify(t *testing.T) { t.Fatalf("Not valid: %+v (%s)", err, err.(*Error).DevInfo) } }) + + fmt.Println("Done") } } From dcf1b9ad41476a36616df52c48ef985f0700eace Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 2 May 2024 09:36:51 +1000 Subject: [PATCH 02/24] temp --- metadata/const.go | 2 +- metadata/decode.go | 166 +++++++++++++++++++++++++++--- metadata/metadata.go | 37 ++++++- metadata/metadata_test.go | 43 ++++++-- metadata/passkey_authenticator.go | 9 ++ metadata/types.go | 7 +- 6 files changed, 234 insertions(+), 30 deletions(-) create mode 100644 metadata/passkey_authenticator.go diff --git a/metadata/const.go b/metadata/const.go index 97ee93a..63a29c4 100644 --- a/metadata/const.go +++ b/metadata/const.go @@ -29,4 +29,4 @@ const ( ) // Metadata is a map of authenticator AAGUIDs to corresponding metadata statements -var Metadata = make(map[uuid.UUID]MetadataBLOBPayloadEntry) +var Metadatas = make(map[uuid.UUID]MetadataBLOBPayloadEntry) diff --git a/metadata/decode.go b/metadata/decode.go index e3232b4..a4db8f4 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -5,34 +5,88 @@ import ( "encoding/base64" "errors" "fmt" - "net/http" - "github.com/go-webauthn/x/revoke" "github.com/golang-jwt/jwt/v5" "github.com/mitchellh/mapstructure" + "io" + "net/http" + "strings" + "time" ) // NewDecoder returns a new metadata decoder. -func NewDecoder() (decoder *Decoder) { - return &Decoder{ +func NewDecoder(opts ...DecoderOption) (decoder *Decoder) { + decoder = &Decoder{ client: &http.Client{}, parser: jwt.NewParser(), hook: mapstructure.ComposeDecodeHookFunc(), } + + for _, opt := range opts { + opt(decoder) + } + + return decoder } type Decoder struct { - client *http.Client - parser *jwt.Parser - hook mapstructure.DecodeHookFunc + client *http.Client + parser *jwt.Parser + hook mapstructure.DecodeHookFunc + skipParserErrors bool +} + +func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, err error) { + metadata = &Metadata{ + Parsed: MetadataBLOBPayload{ + LegalHeader: payload.LegalHeader, + Number: payload.Number, + }, + } + + if metadata.Parsed.NextUpdate, err = time.Parse(time.DateOnly, payload.NextUpdate); err != nil { + return nil, fmt.Errorf("error occurred parsing next update value: %w", err) + } + + var parsed MetadataBLOBPayloadEntry + + for _, entry := range payload.Entries { + if parsed, err = entry.Parse(); err != nil { + metadata.Unparsed = append(metadata.Unparsed, MetadataBLOBPayloadEntryError{ + Error: err, + MetadataBLOBPayloadEntryJSON: entry, + }) + + continue + } + + metadata.Parsed.Entries = append(metadata.Parsed.Entries, parsed) + } + + if n := len(metadata.Unparsed); n != 0 && !d.skipParserErrors { + return metadata, fmt.Errorf("error occured parsing metadata: %d entries had errors during parsing", n) + } + + return metadata, nil } -func (d *Decoder) Decode(blob []byte) (payload *MetadataBLOBPayloadJSON, err error) { +func (d *Decoder) Decode(r io.ReadCloser) (payload *MetadataBLOBPayloadJSON, err error) { + defer r.Close() + + bytes, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + return d.DecodeBytes(bytes) +} + +func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, err error) { payload = &MetadataBLOBPayloadJSON{} var token *jwt.Token - if token, err = d.parser.Parse(string(blob), func(token *jwt.Token) (any, error) { + if token, err = d.parser.Parse(string(bytes), func(token *jwt.Token) (any, error) { // 2. If the x5u attribute is present in the JWT Header, then if _, ok := token.Header[HeaderX509URI].([]any); ok { // never seen an x5u here, although it is in the spec @@ -225,21 +279,105 @@ func validateChain(chain []any, c *http.Client) (bool, error) { return err == nil, err } +const x = ` +// 95e4d58c-056e-4a65-866d-f5a69659e880 / TruU Windows Authenticator: +-----BEGIN CERTIFICATE----- +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTekNDQWZLZ0F3SUJBZ0lV +VzNYSzh5eXdiQVdsaWdsaXhJRjYzZHZxWXk4d0NnWUlLb1pJemowRUF3SXcKZkRF +TE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ01DRU52Ykc5eVlXUnZNUTh3RFFZ +RFZRUUhEQVpFWlc1MgpaWEl4RXpBUkJnTlZCQW9NQ2xSeWRWVXNJRWx1WXk0eElq +QWdCZ05WQkFzTUdVRjFkR2hsYm5ScFkyRjBiM0lnClFYUjBaWE4wWVhScGIyNHhF +REFPQmdOVkJBTU1CM1J5ZFhVdVlXa3dJQmNOTWpNeE1UQXpNakF6TmpVeFdoZ1AK +TWpBMU16RXdNall5TURNMk5URmFNSHd4Q3pBSkJnTlZCQVlUQWxWVE1SRXdEd1lE +VlFRSURBaERiMnh2Y21GawpiekVQTUEwR0ExVUVCd3dHUkdWdWRtVnlNUk13RVFZ +RFZRUUtEQXBVY25WVkxDQkpibU11TVNJd0lBWURWUVFMCkRCbEJkWFJvWlc1MGFX +TmhkRzl5SUVGMGRHVnpkR0YwYVc5dU1SQXdEZ1lEVlFRRERBZDBjblYxTG1GcE1G +a3cKRXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVOQXZidGNjTXI3ai9T +UldtcUlFWlRSV05KeWo2bXNZcgo1bEdlQWdkU0d5QzlPMDM1NlJJZWN1YVZpT3F6 +MER4Z1MxZi81S1BiWnAxdDB5RDJmVlJYOTZOUU1FNHdIUVlEClZSME9CQllFRkE1 +dEwxMGc4OHQycVhsUGxoSVNJMmRJemxhVk1COEdBMVVkSXdRWU1CYUFGQTV0TDEw +Zzg4dDIKcVhsUGxoSVNJMmRJemxhVk1Bd0dBMVVkRXdFQi93UUNNQUF3Q2dZSUtv +Wkl6ajBFQXdJRFJ3QXdSQUlnWGZ1dgpqc3ArNHY1aUdPcW5nVWdPZzFobWJnRlBG +TWdJanlXeENLcXcvZDhDSUZpbUxOWExESXdBK29JYlAxeU9mcUU4CnhrNnE3LzRM +V09WWWtSQUxvQkMyCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= +-----END CERTIFICATE----- + +// 4c0cf95d-2f40-43b5-ba42-4c83a11c04ba / Feitian BioPass FIDO2 Pro Authenticator +-----BEGIN CERTIFICATE----- +MIIB2TCCAX6gAwIBAgIQFQNKW+7zbg/7d+lTyrIWwDAKBggqhkjOPQQDAjBLMQsw +CQYDVQQGEwJVUzEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9sb2dpZXMxHTAbBgNV +BAMMFEZlaXRpYW4gRklETyBSb290IENBMCAXDTIyMDYwODAwMDAwMFoYDzIwNTIw +NjA3MjM1OTU5WjBLMQswCQYDVQQGEwJVUzEdMBsGA1UECgwURmVpdGlhbiBUZWNo +bm9sb2dpZXMxHTAbBgNVBAMMFEZlaXRpYW4gRklETyBSb290IENBMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEsFYEEhiJuqqnMgQjSiivBjV7DGCTf4XBBH/B7uvZ +sKxXShF0L8uDISWUvcExixRs6gB3oldSrjox6L8T94NOzqNCMEAwHQYDVR0OBBYE +FEu9hyYRrRyJzwRYvnDSCIxrFiO3MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgEGMAoGCCqGSM49BAMCA0kAMEYCIQCB0NFQSN0z4lWz/yc36ewrTCzttK/q +FvlaPOKh+T1o6wIhAP0oKKA+cicsDy3Y3n+VlP8eB3PBzMkhvW/9ISXCw+VBMIIB +2DCCAX6gAwIBAgIQBTmk3ZwilFXjsZywHDnMgDAKBggqhkjOPQQDAjBLMQswCQYD +VQQGEwJDTjEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9sb2dpZXMxHTAbBgNVBAMM +FEZlaXRpYW4gRklETyBSb290IENBMCAXDTIyMDYwODAwMDAwMFoYDzIwNTIwNjA3 +MjM1OTU5WjBLMQswCQYDVQQGEwJDTjEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9s +b2dpZXMxHTAbBgNVBAMMFEZlaXRpYW4gRklETyBSb290IENBMFkwEwYHKoZIzj0C +AQYIKoZIzj0DAQcDQgAEnfAKbjvMX1Ey1b6k+WQQdNVMt9JgGWyJ3PvM4BSK5XqT +fo++0oAj/4tnwyIL0HFBR9St+ktjqSXDfjiXAurs86NCMEAwHQYDVR0OBBYEFNGh +mE2Bf8O5a/YHZ71QEv6QRfFUMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMCA0gAMEUCIByRz4OAlRZ9Hz9KV7g2QNtC0C8JxH/xLJY8 +FZEmtJ3sAiEAsreT0+eNkNcUjI9h5OPCoH6NmsOkgvEABJZrF07ADkY= +-----END CERTIFICATE----- + +// ca87cb70-4c1b-4579-a8e8-4efdd7c007e0 / FIDO Alliance TruU Sample FIDO2 Authenticator +-----BEGIN CERTIFICATE----- +LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYekNDQWtlZ0F3SUJBZ0lM +QkFBQUFBQUJJVmhUQ0tJd0RRWUpLb1pJaHZjTkFRRUxCUUF3VERFZ01CNEcKQTFV +RUN4TVhSMnh2WW1Gc1UybG5iaUJTYjI5MElFTkJJQzBnVWpNeEV6QVJCZ05WQkFv +VENrZHNiMkpoYkZOcApaMjR4RXpBUkJnTlZCQU1UQ2tkc2IySmhiRk5wWjI0d0ho +Y05NRGt3TXpFNE1UQXdNREF3V2hjTk1qa3dNekU0Ck1UQXdNREF3V2pCTU1TQXdI +Z1lEVlFRTEV4ZEhiRzlpWVd4VGFXZHVJRkp2YjNRZ1EwRWdMU0JTTXpFVE1CRUcK +QTFVRUNoTUtSMnh2WW1Gc1UybG5iakVUTUJFR0ExVUVBeE1LUjJ4dlltRnNVMmxu +YmpDQ0FTSXdEUVlKS29aSQpodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU13 +bGRwQjVCbmdpRnZYQWc3YUV5aWllL1FWMkVjV3RpSEw4ClJnSkR4N0tLblFSZkpN +c3VTK0ZnZ2tiaFVxc01nVWR3Yk4xazBldjFMS01QZ2owTUs2NlgxN1lVaGhCNXV6 +c1QKZ0hlTUNPRkowbXBpTHg5ZStwWm8zNGtubFRpZkJ0Yyt5Y3NtV1ExejNyREk2 +U1lPZ3hYRzcxdUwwZ1JneWttbQpLUFpwTy9iTHlDaVI1WjJLWVZjM3JIUVUzSFRn +T3U1eUx5NmMrOUM3di9VOUFPRUdNK2lDSzY1VHBqb1djNHpkClFRNGdPc0MwcDZI +cHNrK1FMakpnNlZmTHVRU1NhR2psT0NaZ2RiS2ZkLytSRk8rdUlFbjhyVUFWU05F +Q01XRVoKWHJpWDc2MTN0MlNhZXI5ZndSUHZtMkw3RFd6Z1ZHa1dxUVBhYnVtRGsz +RjJ4bW1GZ2hjQ0F3RUFBYU5DTUVBdwpEZ1lEVlIwUEFRSC9CQVFEQWdFR01BOEdB +MVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJL3dTMytvCkxrVWtyazFR +K21PYWk5N2kzUnU4TUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCTFFOdkFVS3Ir +eUF6djk1WlUKUlVtN2xnQUpRYXl6RTRhR0tBY3p5bXZtZExtNkFDMnVwQXJUOWZI +eEQ0cS9jMmRLZzhkRWUzamdyMjVzYndNcApqak01UmNPTzVMbFhiS3I4RXBic1U4 +WXQ1Q1JzdVpSais5eFRhR2RXUG9PNHp6VWh3OGxvL3M3YXdsT3F6SkNLCjZmQmRS +b3lWM1hwWUtCb3ZIZDdOQURkQmorMUViZGRUS0pkKzgyY0VIaFhYaXBhMDA5NU1K +NlJNRzNOemR2UVgKbWNJZmVnN2pMUWl0Q2h3cy96eXJWUTRQa1g0MjY4TlhTYjdo +TGkxOFlJdkRRVkVUSTUzTzl6SnJsQUdvbWVjcwpNeDg2T3lYU2hrRE9PeXlHZU1s +aEx4UzY3dHRWYjkrRTdnVUpUYjBvMkhMTzAySlFaUjdya3BlRE1kbXp0Y3BICldE +OWYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== +-----END CERTIFICATE----- +` + func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, err error) { var n int raw := make([]byte, base64.StdEncoding.DecodedLen(len(value))) - if n, err = base64.StdEncoding.Decode(raw, []byte(value)); err != nil { + if n, err = base64.StdEncoding.Decode(raw, []byte(strings.TrimSpace(value))); err != nil { return nil, fmt.Errorf("error occurred parsing *x509.certificate: error occurred decoding base64 data: %w", err) } if certificate, err = x509.ParseCertificate(raw[:n]); err != nil { - fmt.Println(err) - - return nil, nil - //return nil, fmt.Errorf("error occurred parsing *x509.certificate: error occurred parsing certificate: %w", err) + fmt.Println("failed to parse cert", value) + return nil, err } return certificate, nil } + +type DecoderOption func(decoder *Decoder) + +func WithSkipParserErrors() DecoderOption { + return func(decoder *Decoder) { + decoder.skipParserErrors = true + } +} diff --git a/metadata/metadata.go b/metadata/metadata.go index 147e054..49a1935 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -4,11 +4,34 @@ import ( "crypto/x509" "fmt" "net/url" + "strings" "time" "github.com/google/uuid" ) +type Metadata struct { + Parsed MetadataBLOBPayload + Unparsed []MetadataBLOBPayloadEntryError +} + +func (m *Metadata) ToMap() (metadata map[uuid.UUID]MetadataBLOBPayloadEntry) { + metadata = make(map[uuid.UUID]MetadataBLOBPayloadEntry) + + for _, entry := range m.Parsed.Entries { + if entry.AaGUID.ID() != 0 { + metadata[entry.AaGUID] = entry + } + } + + return metadata +} + +type MetadataBLOBPayloadEntryError struct { + Error error + MetadataBLOBPayloadEntryJSON +} + // MetadataBLOBPayload is a structure representing the MetadataBLOBPayload MDS3 dictionary. // // See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary @@ -338,8 +361,10 @@ func (j MetadataStatementJSON) Parse() (statement MetadataStatement, err error) var icon *url.URL - if icon, err = url.ParseRequestURI(j.Icon); err != nil { - return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing icon value: %w", j.Description, err) + if len(j.Icon) != 0 { + if icon, err = url.ParseRequestURI(j.Icon); err != nil { + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing icon value: %w", j.Description, err) + } } var info AuthenticatorGetInfo @@ -505,7 +530,13 @@ func (j StatusReportJSON) Parse() (report StatusReport, err error) { if len(j.URL) != 0 { if uri, err = url.ParseRequestURI(j.URL); err != nil { - return report, fmt.Errorf("error occurred parsing URL value: %w", err) + if !strings.HasPrefix(j.URL, "http") { + var e error + + if uri, e = url.ParseRequestURI(fmt.Sprintf("https://%s", j.URL)); e != nil { + return report, fmt.Errorf("error occurred parsing URL value: %w", err) + } + } } } diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 8210b7a..b4ef3b3 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -3,6 +3,8 @@ package metadata import ( "bytes" "encoding/json" + "fmt" + "github.com/stretchr/testify/require" "io" "net/http" "testing" @@ -15,6 +17,27 @@ import ( ) func TestProductionMetadataTOCParsing(t *testing.T) { + decoder := NewDecoder(WithSkipParserErrors()) + + client := &http.Client{} + + res, err := client.Get(ProductionMDSURL) + require.NoError(t, err) + + payload, err := decoder.Decode(res.Body) + require.NoError(t, err) + + var metadata *Metadata + + metadata, err = decoder.Parse(payload) + require.NoError(t, err) + + for _, perr := range metadata.Unparsed { + fmt.Println(perr.Error) + } +} + +func TestProductionMetadataTOCParsingx(t *testing.T) { if err := PopulateMetadata(ProductionMDSURL, true); err != nil { t.Fatal(err) } @@ -67,12 +90,12 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { metadata := make(map[uuid.UUID]MetadataBLOBPayloadEntryJSON) for _, endpoint := range endpoints { - bytes, err := downloadBytes(endpoint, httpClient) + res, err := httpClient.Get(endpoint) if err != nil { t.Fatal(err) } - blob, err := decoder.Decode(bytes) + blob, err := decoder.Decode(res.Body) if err != nil { if me, ok := err.(*MetadataError); ok { t.Log(me.Details) @@ -127,12 +150,14 @@ func TestExampleMetadataTOCParsing(t *testing.T) { exampleMetadataBLOBBytes := bytes.NewBufferString(exampleMetadataBLOB) - decoder := NewDecoder() + decoder := NewDecoder(WithSkipParserErrors()) - _, err := decoder.Decode(exampleMetadataBLOBBytes.Bytes()) - if err != nil { - t.Fail() - } + payload, err := decoder.DecodeBytes(exampleMetadataBLOBBytes.Bytes()) + require.NoError(t, err) + + _, err = decoder.Parse(payload) + + require.NoError(t, err) } func TestIsUndesiredAuthenticatorStatus(t *testing.T) { @@ -277,6 +302,10 @@ func TestAlgKeyMatch(t *testing.T) { } } +func download(url string, c *http.Client) { + +} + func downloadBytes(url string, c *http.Client) ([]byte, error) { res, err := c.Get(url) if err != nil { diff --git a/metadata/passkey_authenticator.go b/metadata/passkey_authenticator.go new file mode 100644 index 0000000..b057600 --- /dev/null +++ b/metadata/passkey_authenticator.go @@ -0,0 +1,9 @@ +package metadata + +type PasskeyAuthenticator map[string]PassKeyAuthenticatorAAGUID + +type PassKeyAuthenticatorAAGUID struct { + Name string `json:"name"` + IconDark string `json:"icon_dark"` + IconLight string `json:"icon_light"` +} diff --git a/metadata/types.go b/metadata/types.go index d74a609..ef1310e 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -2,7 +2,6 @@ package metadata import ( "context" - "fmt" "io" "net/http" "reflect" @@ -14,7 +13,7 @@ import ( ) type Provider interface { - GetMetadataBLOBPayloadEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) + GetMetaData(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) } type PublicKeyCredentialParameters struct { @@ -249,8 +248,6 @@ func PopulateMetadata(url string, skipInvalid bool) error { } for _, entry := range blob.Entries { - fmt.Println("parsing", entry.AaGUID, entry.MetadataStatement.Description) - parsed, err := entry.Parse() if err != nil { if skipInvalid { @@ -260,7 +257,7 @@ func PopulateMetadata(url string, skipInvalid bool) error { return err } - Metadata[parsed.AaGUID] = parsed + Metadatas[parsed.AaGUID] = parsed } return err From 762566024327ba1b71b15d1f0d03ea0d5322e2e6 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 15 May 2024 06:53:46 +1000 Subject: [PATCH 03/24] temp --- metadata/decode.go | 32 +++++++++++++++++--------------- metadata/metadata_test.go | 4 ++-- metadata/types.go | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index a4db8f4..cc6fd80 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -5,13 +5,14 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/go-webauthn/x/revoke" - "github.com/golang-jwt/jwt/v5" - "github.com/mitchellh/mapstructure" "io" "net/http" "strings" "time" + + "github.com/go-webauthn/x/revoke" + "github.com/golang-jwt/jwt/v5" + "github.com/mitchellh/mapstructure" ) // NewDecoder returns a new metadata decoder. @@ -30,10 +31,10 @@ func NewDecoder(opts ...DecoderOption) (decoder *Decoder) { } type Decoder struct { - client *http.Client - parser *jwt.Parser - hook mapstructure.DecodeHookFunc - skipParserErrors bool + client *http.Client + parser *jwt.Parser + hook mapstructure.DecodeHookFunc + ignoreEntryParsingErrors bool } func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, err error) { @@ -63,7 +64,7 @@ func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, e metadata.Parsed.Entries = append(metadata.Parsed.Entries, parsed) } - if n := len(metadata.Unparsed); n != 0 && !d.skipParserErrors { + if n := len(metadata.Unparsed); n != 0 && !d.ignoreEntryParsingErrors { return metadata, fmt.Errorf("error occured parsing metadata: %d entries had errors during parsing", n) } @@ -107,7 +108,7 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, e } // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. - if valid, err = validateChain(chain, d.client); !valid || err != nil { + if valid, err = validateChain(chain); !valid || err != nil { return nil, err } @@ -154,7 +155,7 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, e return payload, nil } -func unmarshalMDSBLOB(body []byte, c *http.Client) (MetadataBLOBPayloadJSON, error) { +func unmarshalMDSBLOB(body []byte) (MetadataBLOBPayloadJSON, error) { var payload MetadataBLOBPayloadJSON token, err := jwt.Parse(string(body), func(token *jwt.Token) (any, error) { @@ -174,7 +175,7 @@ func unmarshalMDSBLOB(body []byte, c *http.Client) (MetadataBLOBPayloadJSON, err } // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. - valid, err := validateChain(chain, c) + valid, err := validateChain(chain) if !valid || err != nil { return nil, err } @@ -209,7 +210,7 @@ func unmarshalMDSBLOB(body []byte, c *http.Client) (MetadataBLOBPayloadJSON, err return payload, err } -func validateChain(chain []any, c *http.Client) (bool, error) { +func validateChain(chain []any) (bool, error) { oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(MDSRoot))) nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(MDSRoot)) @@ -367,7 +368,6 @@ func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, } if certificate, err = x509.ParseCertificate(raw[:n]); err != nil { - fmt.Println("failed to parse cert", value) return nil, err } @@ -376,8 +376,10 @@ func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, type DecoderOption func(decoder *Decoder) -func WithSkipParserErrors() DecoderOption { +// WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for +// these entries will exist as an unparsed entry. +func WithIgnoreEntryParsingErrors() DecoderOption { return func(decoder *Decoder) { - decoder.skipParserErrors = true + decoder.ignoreEntryParsingErrors = true } } diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index b4ef3b3..458a5d8 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -17,7 +17,7 @@ import ( ) func TestProductionMetadataTOCParsing(t *testing.T) { - decoder := NewDecoder(WithSkipParserErrors()) + decoder := NewDecoder(WithIgnoreEntryParsingErrors()) client := &http.Client{} @@ -150,7 +150,7 @@ func TestExampleMetadataTOCParsing(t *testing.T) { exampleMetadataBLOBBytes := bytes.NewBufferString(exampleMetadataBLOB) - decoder := NewDecoder(WithSkipParserErrors()) + decoder := NewDecoder(WithIgnoreEntryParsingErrors()) payload, err := decoder.DecodeBytes(exampleMetadataBLOBBytes.Bytes()) require.NoError(t, err) diff --git a/metadata/types.go b/metadata/types.go index ef1310e..ac3acb9 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -242,7 +242,7 @@ func PopulateMetadata(url string, skipInvalid bool) error { return err } - blob, err := unmarshalMDSBLOB(body, c) + blob, err := unmarshalMDSBLOB(body) if err != nil { return err } From 85e4b87ff68ef733372a0c6a8eff6cfe806ef194 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Thu, 2 May 2024 09:36:51 +1000 Subject: [PATCH 04/24] temp --- metadata/const.go | 5 --- metadata/memory.go | 62 +++++++++++++++++++++++++++++ metadata/metadata.go | 8 ++-- metadata/metadata_test.go | 8 +--- metadata/types.go | 73 +++++++++++++--------------------- protocol/attestation.go | 77 ++++++++++++++++++++++-------------- protocol/attestation_test.go | 8 +--- protocol/credential.go | 5 ++- protocol/credential_test.go | 2 +- webauthn/registration.go | 2 +- webauthn/types.go | 31 +++++++++++++++ 11 files changed, 181 insertions(+), 100 deletions(-) create mode 100644 metadata/memory.go diff --git a/metadata/const.go b/metadata/const.go index 63a29c4..8ef7f59 100644 --- a/metadata/const.go +++ b/metadata/const.go @@ -1,7 +1,5 @@ package metadata -import "github.com/google/uuid" - const ( // https://secure.globalsign.com/cacert/root-r3.crt ProductionMDSRoot = "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpHWD9f" @@ -27,6 +25,3 @@ const ( HeaderX509URI = "x5u" HeaderX509Certificate = "x5c" ) - -// Metadata is a map of authenticator AAGUIDs to corresponding metadata statements -var Metadatas = make(map[uuid.UUID]MetadataBLOBPayloadEntry) diff --git a/metadata/memory.go b/metadata/memory.go new file mode 100644 index 0000000..8411e8c --- /dev/null +++ b/metadata/memory.go @@ -0,0 +1,62 @@ +package metadata + +import ( + "context" + + "github.com/google/uuid" +) + +// NewMemoryProvider returns a new memory provider given a map, list of undesired AuthenticatorStatus types, and a +// conformance requirement boolean. +// +// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable +// this functionality. +func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired []AuthenticatorStatus, required bool) *MemoryProvider { + if undesired == nil { + undesired = make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) + + for i := range defaultUndesiredAuthenticatorStatus { + undesired[i] = defaultUndesiredAuthenticatorStatus[i] + } + } + + return &MemoryProvider{ + mds: mds, + undesired: undesired, + require: required, + } +} + +type MemoryProvider struct { + mds map[uuid.UUID]*MetadataBLOBPayloadEntry + undesired []AuthenticatorStatus + require bool +} + +func (p *MemoryProvider) GetRequireConformance(ctx context.Context) (require bool) { + return p.require +} + +func (p *MemoryProvider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) { + if p.mds == nil { + return nil, ErrNotInitialized + } + + var ok bool + + if entry, ok = p.mds[aaguid]; ok { + return entry, nil + } + + return nil, nil +} + +func (p *MemoryProvider) GetIsUndesiredAuthenticatorStatus(ctx context.Context, status AuthenticatorStatus) (isUndesiredAuthenticatorStatus bool) { + for _, s := range p.undesired { + if s == status { + return true + } + } + + return false +} diff --git a/metadata/metadata.go b/metadata/metadata.go index 49a1935..8e90d2a 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -15,12 +15,12 @@ type Metadata struct { Unparsed []MetadataBLOBPayloadEntryError } -func (m *Metadata) ToMap() (metadata map[uuid.UUID]MetadataBLOBPayloadEntry) { - metadata = make(map[uuid.UUID]MetadataBLOBPayloadEntry) +func (m *Metadata) ToMap() (metadata map[uuid.UUID]*MetadataBLOBPayloadEntry) { + metadata = make(map[uuid.UUID]*MetadataBLOBPayloadEntry) for _, entry := range m.Parsed.Entries { if entry.AaGUID.ID() != 0 { - metadata[entry.AaGUID] = entry + metadata[entry.AaGUID] = &entry } } @@ -240,7 +240,7 @@ type MetadataStatement struct { PublicKeyAlgAndEncodings []PublicKeyAlgAndEncoding // The supported attestation type(s). - AttestationTypes []AuthenticatorAttestationType + AttestationTypes AuthenticatorAttestationTypes // A list of alternative VerificationMethodANDCombinations. UserVerificationDetails [][]VerificationMethodDescriptor diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 458a5d8..efce25b 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/stretchr/testify/require" "io" "net/http" "testing" @@ -12,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/go-webauthn/webauthn/protocol/webauthncose" ) @@ -37,12 +37,6 @@ func TestProductionMetadataTOCParsing(t *testing.T) { } } -func TestProductionMetadataTOCParsingx(t *testing.T) { - if err := PopulateMetadata(ProductionMDSURL, true); err != nil { - t.Fatal(err) - } -} - func TestConformanceMetadataTOCParsing(t *testing.T) { MDSRoot = ConformanceMDSRoot Conformance = true diff --git a/metadata/types.go b/metadata/types.go index ac3acb9..7b56aa0 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -2,10 +2,8 @@ package metadata import ( "context" - "io" - "net/http" + "errors" "reflect" - "time" "github.com/google/uuid" @@ -13,14 +11,37 @@ import ( ) type Provider interface { - GetMetaData(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) + // GetRequireConformance returns true if this provider requires conformance. + GetRequireConformance(ctx context.Context) (require bool) + + // GetEntry returns a MDS3 payload entry given a AAGUID. This + GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) + + // GetIsUndesiredAuthenticatorStatus returns true if the provided AuthenticatorStatus is not desired. + GetIsUndesiredAuthenticatorStatus(ctx context.Context, status AuthenticatorStatus) (isUndesiredAuthenticatorStatus bool) } +var ( + ErrNotInitialized = errors.New("metadata: not initialized") +) + type PublicKeyCredentialParameters struct { Type string `json:"type"` Alg webauthncose.COSEAlgorithmIdentifier `json:"alg"` } +type AuthenticatorAttestationTypes []AuthenticatorAttestationType + +func (t AuthenticatorAttestationTypes) HasBasicFull() bool { + for _, a := range t { + if a == BasicFull || a == AttCA { + return true + } + } + + return false +} + // AuthenticatorAttestationType - The ATTESTATION constants are 16 bit long integers indicating the specific attestation that authenticator supports. // Each constant has a case-sensitive string representation (in quotes), which is used in the authoritative metadata for FIDO authenticators. type AuthenticatorAttestationType string @@ -77,8 +98,8 @@ const ( FidoCertifiedL3plus AuthenticatorStatus = "FIDO_CERTIFIED_L3plus" ) -// UndesiredAuthenticatorStatus is an array of undesirable authenticator statuses -var UndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ +// defaultUndesiredAuthenticatorStatus is an array of undesirable authenticator statuses +var defaultUndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ AttestationKeyCompromise, UserVerificationBypass, UserKeyRemoteCompromise, @@ -88,7 +109,7 @@ var UndesiredAuthenticatorStatus = [...]AuthenticatorStatus{ // IsUndesiredAuthenticatorStatus returns whether the supplied authenticator status is desirable or not func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool { - for _, s := range UndesiredAuthenticatorStatus { + for _, s := range defaultUndesiredAuthenticatorStatus { if s == status { return true } @@ -224,41 +245,3 @@ var ( func (err *MetadataError) Error() string { return err.Details } - -func PopulateMetadata(url string, skipInvalid bool) error { - c := &http.Client{ - Timeout: time.Second * 30, - } - - res, err := c.Get(url) - if err != nil { - return err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return err - } - - blob, err := unmarshalMDSBLOB(body) - if err != nil { - return err - } - - for _, entry := range blob.Entries { - parsed, err := entry.Parse() - if err != nil { - if skipInvalid { - continue - } - - return err - } - - Metadatas[parsed.AaGUID] = parsed - } - - return err -} diff --git a/protocol/attestation.go b/protocol/attestation.go index 5ad5f8e..31c7c99 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -1,6 +1,7 @@ package protocol import ( + "context" "crypto/sha256" "crypto/x509" "encoding/json" @@ -120,7 +121,7 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon // // Steps 9 through 12 are verified against the auth data. These steps are identical to 11 through 14 for assertion so we // handle them with AuthData. -func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool) error { +func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool, mds metadata.Provider) error { rpIDHash := sha256.Sum256([]byte(relyingPartyID)) // Begin Step 9 through 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP. @@ -157,50 +158,68 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client // Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using // the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized // client data computed in step 7. - attestationType, x5c, err := formatHandler(*attestationObject, clientDataHash) + attestationType, x5cs, err := formatHandler(*attestationObject, clientDataHash) if err != nil { return err.(*Error).WithInfo(attestationType) } - aaguid, err := uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID) - if err != nil { + var ( + aaguid uuid.UUID + entry *metadata.MetadataBLOBPayloadEntry + ) + + if aaguid, err = uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID); err != nil { return err } - if meta, ok := metadata.Metadata[aaguid]; ok { - for _, s := range meta.StatusReports { - if metadata.IsUndesiredAuthenticatorStatus(s.Status) { - return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") - } + if mds == nil { + return nil + } + + ctx := context.Background() + + if entry, err = mds.GetEntry(ctx, aaguid); err != nil { + return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred: %+v", err)).WithDetails(fmt.Sprintf("Error occurred looking up entry for AAGUID %s", aaguid.String())) + } + + if entry == nil { + if mds.GetRequireConformance(ctx) { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String())) } - if x5c != nil { - var x5cAtt *x509.Certificate + return nil + } - if x5cAtt, err = x509.ParseCertificate(x5c[0].([]byte)); err != nil { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") - } + for _, s := range entry.StatusReports { + if mds.GetIsUndesiredAuthenticatorStatus(ctx, s.Status) { + return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") + } + } - if x5cAtt.Subject.CommonName != x5cAtt.Issuer.CommonName { - var hasBasicFull = false + if x5cs != nil { + var ( + x5c *x509.Certificate + raw []byte + ok bool + ) - for _, a := range meta.MetadataStatement.AttestationTypes { - if a == metadata.BasicFull || a == metadata.AttCA { - hasBasicFull = true - } - } + if raw, ok = x5cs[0].([]byte); !ok { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") + } + + if x5c, err = x509.ParseCertificate(raw); err != nil { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") + } - if !hasBasicFull { - return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") - } + if x5c.Subject.CommonName != x5c.Issuer.CommonName { + if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { + return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + } - if _, err = x5cAtt.Verify(meta.MetadataStatement.Verifier()); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) - } + if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) } } - } else if metadata.Conformance { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String())) } return nil diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index 786009f..cdfcf0c 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -5,14 +5,10 @@ import ( "encoding/json" "fmt" "testing" - - "github.com/stretchr/testify/assert" - - "github.com/go-webauthn/webauthn/metadata" ) func TestAttestationVerify(t *testing.T) { - assert.NoError(t, metadata.PopulateMetadata(metadata.ProductionMDSURL, true)) + //assert.NoError(t, metadata.PopulateMetadata(metadata.ProductionMDSURL, true)) for i := range testAttestationOptions { t.Run(fmt.Sprintf("Running test %d", i), func(t *testing.T) { @@ -39,7 +35,7 @@ func TestAttestationVerify(t *testing.T) { pcc.Response = *parsedAttestationResponse // Test Base Verification - err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode) + err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode, nil) if err != nil { t.Fatalf("Not valid: %+v (%s)", err, err.(*Error).DevInfo) } diff --git a/protocol/credential.go b/protocol/credential.go index d532e2b..e8eeb2a 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -3,6 +3,7 @@ package protocol import ( "crypto/sha256" "encoding/base64" + "github.com/go-webauthn/webauthn/metadata" "io" "net/http" ) @@ -131,7 +132,7 @@ func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData // Verify the Client and Attestation data. // // Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential) -func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode) error { +func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, mds metadata.Provider) error { // Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) if verifyError != nil { @@ -147,7 +148,7 @@ func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUs // We do the above step while parsing and decoding the CredentialCreationResponse // Handle steps 9 through 14 - This verifies the attestation object. - verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser) + verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser, mds) if verifyError != nil { return verifyError } diff --git a/protocol/credential_test.go b/protocol/credential_test.go index 26d0256..dab74c3 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -240,7 +240,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) { Response: tt.fields.Response, Raw: tt.fields.Raw, } - if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode); (err != nil) != tt.wantErr { + if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, nil); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialCreationData.Verify() error = %+v, wantErr %v", err, tt.wantErr) } }) diff --git a/webauthn/registration.go b/webauthn/registration.go index a0d6e3a..6ed8236 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -224,7 +224,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse shouldVerifyUser := session.UserVerification == protocol.VerificationRequired - invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode) + invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.MetaData) if invalidErr != nil { return nil, invalidErr } diff --git a/webauthn/types.go b/webauthn/types.go index 41e4999..cd53db6 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -2,6 +2,7 @@ package webauthn import ( "fmt" + "github.com/go-webauthn/webauthn/metadata" "net/url" "time" @@ -62,6 +63,8 @@ type Config struct { // Timeouts configures various timeouts. Timeouts TimeoutsConfig + MetaData metadata.Provider + validated bool } @@ -136,6 +139,34 @@ func (config *Config) validate() error { return nil } +func (c *Config) GetRPID() string { + return c.RPID +} + +func (c *Config) GetOrigins() []string { + return c.RPOrigins +} + +func (c *Config) GetTopOrigins() []string { + return c.RPTopOrigins +} + +func (c *Config) GetTopOriginVerificationMode() protocol.TopOriginVerificationMode { + return c.RPTopOriginVerificationMode +} + +func (c *Config) GetMetaDataProvider() metadata.Provider { + return c.MetaData +} + +type ConfigProvider interface { + GetRPID() string + GetOrigins() []string + GetTopOrigins() []string + GetTopOriginVerificationMode() protocol.TopOriginVerificationMode + GetMetaDataProvider() metadata.Provider +} + // User is an interface with the Relying Party's User entry and provides the fields and methods needed for WebAuthn // registration operations. type User interface { From da08d903a586f705d5e1b8a81424fb3be03766ab Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 18 May 2024 11:32:36 +1000 Subject: [PATCH 05/24] temp --- protocol/attestation.go | 4 +-- protocol/attestation_androidkey.go | 8 ++--- protocol/attestation_androidkey_test.go | 2 +- protocol/attestation_apple.go | 4 +-- protocol/attestation_apple_test.go | 2 +- protocol/attestation_packed.go | 10 +++---- protocol/attestation_packed_test.go | 2 +- protocol/attestation_safetynet.go | 27 +++++++---------- protocol/attestation_safetynet_test.go | 2 +- protocol/attestation_test.go | 2 +- protocol/attestation_tpm.go | 16 +++++----- protocol/attestation_tpm_test.go | 40 ++++++++++++------------- protocol/attestation_u2f.go | 6 ++-- protocol/attestation_u2f_test.go | 2 +- protocol/const.go | 11 +++++++ protocol/credential.go | 3 +- protocol/webauthncose/webauthncose.go | 1 + webauthn/types.go | 2 +- 18 files changed, 76 insertions(+), 68 deletions(-) create mode 100644 protocol/const.go diff --git a/protocol/attestation.go b/protocol/attestation.go index 31c7c99..f77c5cb 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -75,7 +75,7 @@ type AttestationObject struct { AttStatement map[string]any `json:"attStmt,omitempty"` } -type attestationFormatValidationHandler func(AttestationObject, []byte) (string, []any, error) +type attestationFormatValidationHandler func(AttestationObject, []byte, metadata.Provider) (string, []any, error) var attestationRegistry = make(map[AttestationFormat]attestationFormatValidationHandler) @@ -158,7 +158,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client // Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using // the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized // client data computed in step 7. - attestationType, x5cs, err := formatHandler(*attestationObject, clientDataHash) + attestationType, x5cs, err := formatHandler(*attestationObject, clientDataHash, mds) if err != nil { return err.(*Error).WithInfo(attestationType) } diff --git a/protocol/attestation_androidkey.go b/protocol/attestation_androidkey.go index b8551ca..2201303 100644 --- a/protocol/attestation_androidkey.go +++ b/protocol/attestation_androidkey.go @@ -29,26 +29,26 @@ func init() { // } // // Specification: §8.4. Android Key Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-key-attestation) -func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifyAndroidKeyFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) { // Given the verification procedure inputs attStmt, authenticatorData and clientDataHash, the verification procedure is as follows: // §8.4.1. Verify that attStmt is valid CBOR conforming to the syntax defined above and perform CBOR decoding on it to extract // the contained fields. // Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm // used to generate the attestation signature. - alg, present := att.AttStatement["alg"].(int64) + alg, present := att.AttStatement[stmtAlgorithm].(int64) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value") } // Get the sig value - A byte string containing the attestation signature. - sig, present := att.AttStatement["sig"].([]byte) + sig, present := att.AttStatement[stmtSignature].([]byte) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value") } // If x5c is not present, return an error - x5c, x509present := att.AttStatement["x5c"].([]any) + x5c, x509present := att.AttStatement[stmtX5C].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value") diff --git a/protocol/attestation_androidkey_test.go b/protocol/attestation_androidkey_test.go index 73266c8..e6fd426 100644 --- a/protocol/attestation_androidkey_test.go +++ b/protocol/attestation_androidkey_test.go @@ -49,7 +49,7 @@ func TestVerifyAndroidKeyFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _, err := verifyAndroidKeyFormat(tt.args.att, tt.args.clientDataHash) + got, _, err := verifyAndroidKeyFormat(tt.args.att, tt.args.clientDataHash, nil) if (err != nil) != tt.wantErr { t.Errorf("verifyAndroidKeyFormat() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/protocol/attestation_apple.go b/protocol/attestation_apple.go index 0e7be7c..c828e7b 100644 --- a/protocol/attestation_apple.go +++ b/protocol/attestation_apple.go @@ -31,12 +31,12 @@ func init() { // } // // Specification: §8.8. Apple Anonymous Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-apple-anonymous-attestation) -func verifyAppleFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifyAppleFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) { // Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined // above and perform CBOR decoding on it to extract the contained fields. // If x5c is not present, return an error - x5c, x509present := att.AttStatement["x5c"].([]any) + x5c, x509present := att.AttStatement[stmtX5C].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrAttestationFormat.WithDetails("Error retrieving x5c value") diff --git a/protocol/attestation_apple_test.go b/protocol/attestation_apple_test.go index fc42ff6..d795f95 100644 --- a/protocol/attestation_apple_test.go +++ b/protocol/attestation_apple_test.go @@ -37,7 +37,7 @@ func Test_verifyAppleFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _, err := verifyAppleFormat(tt.args.att, tt.args.clientDataHash) + got, _, err := verifyAppleFormat(tt.args.att, tt.args.clientDataHash, nil) if (err != nil) != tt.wantErr { t.Errorf("verifyAppleFormat() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/protocol/attestation_packed.go b/protocol/attestation_packed.go index c33a904..7176947 100644 --- a/protocol/attestation_packed.go +++ b/protocol/attestation_packed.go @@ -34,26 +34,26 @@ func init() { // } // // Specification: §8.2. Packed Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-packed-attestation) -func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifyPackedFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) { // Step 1. Verify that attStmt is valid CBOR conforming to the syntax defined // above and perform CBOR decoding on it to extract the contained fields. // Get the alg value - A COSEAlgorithmIdentifier containing the identifier of the algorithm // used to generate the attestation signature. - alg, present := att.AttStatement["alg"].(int64) + alg, present := att.AttStatement[stmtAlgorithm].(int64) if !present { return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving alg value") } // Get the sig value - A byte string containing the attestation signature. - sig, present := att.AttStatement["sig"].([]byte) + sig, present := att.AttStatement[stmtSignature].([]byte) if !present { return string(AttestationFormatPacked), nil, ErrAttestationFormat.WithDetails("Error retrieving sig value") } // Step 2. If x5c is present, this indicates that the attestation type is not ECDAA. - x5c, x509present := att.AttStatement["x5c"].([]any) + x5c, x509present := att.AttStatement[stmtX5C].([]any) if x509present { // Handle Basic Attestation steps for the x509 Certificate return handleBasicAttestation(sig, clientDataHash, att.RawAuthData, att.AuthData.AttData.AAGUID, alg, x5c) @@ -61,7 +61,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [ // Step 3. If ecdaaKeyId is present, then the attestation type is ECDAA. // Also make sure the we did not have an x509 then - ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte) + ecdaaKeyID, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte) if ecdaaKeyPresent { // Handle ECDAA Attestation steps for the x509 Certificate return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID) diff --git a/protocol/attestation_packed_test.go b/protocol/attestation_packed_test.go index 66e8b6b..7f22266 100644 --- a/protocol/attestation_packed_test.go +++ b/protocol/attestation_packed_test.go @@ -61,7 +61,7 @@ func Test_verifyPackedFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _, err := verifyPackedFormat(tt.args.att, tt.args.clientDataHash) + got, _, err := verifyPackedFormat(tt.args.att, tt.args.clientDataHash, nil) if (err != nil) != tt.wantErr { t.Errorf("verifyPackedFormat() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index 954541d..cc55a0f 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -2,6 +2,7 @@ package protocol import ( "bytes" + "context" "crypto/sha256" "crypto/x509" "encoding/base64" @@ -40,7 +41,7 @@ type SafetyNetResponse struct { // authenticators SHOULD make use of the Android Key Attestation when available, even if the SafetyNet API is also present. // // Specification: §8.5. Android SafetyNet Attestation Statement Format (https://www.w3.org/TR/webauthn/#sctn-android-safetynet-attestation) -func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte, mds metadata.Provider) (string, []any, error) { // The syntax of an Android Attestation statement is defined as follows: // $$attStmtType //= ( // fmt: "android-safetynet", @@ -57,7 +58,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string // We have done this // §8.5.2 Verify that response is a valid SafetyNet response of version ver. - version, present := att.AttStatement["ver"].(string) + version, present := att.AttStatement[stmtVersion].(string) if !present { return "", nil, ErrAttestationFormat.WithDetails("Unable to find the version of SafetyNet") } @@ -74,7 +75,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string } token, err := jwt.Parse(string(response), func(token *jwt.Token) (any, error) { - chain := token.Header["x5c"].([]any) + chain := token.Header[stmtX5C].([]any) o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) @@ -108,7 +109,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string } // §8.5.4 Let attestationCert be the attestation certificate (https://www.w3.org/TR/webauthn/#attestation-certificate) - certChain := token.Header["x5c"].([]any) + certChain := token.Header[stmtX5C].([]any) l := make([]byte, base64.StdEncoding.DecodedLen(len(certChain[0].(string)))) n, err := base64.StdEncoding.Decode(l, []byte(certChain[0].(string))) @@ -132,19 +133,13 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte) (string return "", nil, ErrInvalidAttestation.WithDetails("ctsProfileMatch attribute of the JWT payload is false") } - // Verify sanity of timestamp in the payload - now := time.Now() - oneMinuteAgo := now.Add(-time.Minute) - - if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(now) { - // zero tolerance for post-dated timestamps + if t := time.Unix(safetyNetResponse.TimestampMs/1000, 0); t.After(time.Now()) { + // Zero tolerance for post-dated timestamps. return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time") - } else if t.Before(oneMinuteAgo) { - // allow old timestamp for testing purposes - // TODO: Make this user configurable - msg := "SafetyNet response with timestamp before one minute ago" - if metadata.Conformance { - return "", nil, ErrInvalidAttestation.WithDetails(msg) + } else if t.Before(time.Now().Add(-time.Minute)) { + // Small tolerance for pre-dated timestamps. + if mds != nil && mds.GetRequireConformance(context.Background()) { + return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago") } } diff --git a/protocol/attestation_safetynet_test.go b/protocol/attestation_safetynet_test.go index d40ba30..0f4626b 100644 --- a/protocol/attestation_safetynet_test.go +++ b/protocol/attestation_safetynet_test.go @@ -38,7 +38,7 @@ func Test_verifySafetyNetFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, got1, err := verifySafetyNetFormat(tt.args.att, tt.args.clientDataHash) + got, got1, err := verifySafetyNetFormat(tt.args.att, tt.args.clientDataHash, nil) if (err != nil) != tt.wantErr { t.Errorf("verifySafetyNetFormat() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index cdfcf0c..cbb2d47 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -82,7 +82,7 @@ func TestPackedAttestationVerification(t *testing.T) { // Test Packed Verification. Unpack args. clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) - _, _, err := verifyPackedFormat(pcc.Response.AttestationObject, clientDataHash[:]) + _, _, err := verifyPackedFormat(pcc.Response.AttestationObject, clientDataHash[:], nil) if err != nil { t.Fatalf("Not valid: %+v", err) } diff --git a/protocol/attestation_tpm.go b/protocol/attestation_tpm.go index e86881c..e077739 100644 --- a/protocol/attestation_tpm.go +++ b/protocol/attestation_tpm.go @@ -19,14 +19,14 @@ func init() { RegisterAttestationFormat(AttestationFormatTPM, verifyTPMFormat) } -func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifyTPMFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) { // Given the verification procedure inputs attStmt, authenticatorData // and clientDataHash, the verification procedure is as follows // Verify that attStmt is valid CBOR conforming to the syntax defined // above and perform CBOR decoding on it to extract the contained fields - ver, present := att.AttStatement["ver"].(string) + ver, present := att.AttStatement[stmtVersion].(string) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving ver value") } @@ -35,35 +35,35 @@ func verifyTPMFormat(att AttestationObject, clientDataHash []byte) (string, []an return "", nil, ErrAttestationFormat.WithDetails("WebAuthn only supports TPM 2.0 currently") } - alg, present := att.AttStatement["alg"].(int64) + alg, present := att.AttStatement[stmtAlgorithm].(int64) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving alg value") } coseAlg := webauthncose.COSEAlgorithmIdentifier(alg) - x5c, x509present := att.AttStatement["x5c"].([]any) + x5c, x509present := att.AttStatement[stmtX5C].([]any) if !x509present { // Handle Basic Attestation steps for the x509 Certificate return "", nil, ErrNotImplemented } - _, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte) + _, ecdaaKeyPresent := att.AttStatement[stmtECDAAKID].([]byte) if ecdaaKeyPresent { return "", nil, ErrNotImplemented } - sigBytes, present := att.AttStatement["sig"].([]byte) + sigBytes, present := att.AttStatement[stmtSignature].([]byte) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving sig value") } - certInfoBytes, present := att.AttStatement["certInfo"].([]byte) + certInfoBytes, present := att.AttStatement[stmtCertInfo].([]byte) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving certInfo value") } - pubAreaBytes, present := att.AttStatement["pubArea"].([]byte) + pubAreaBytes, present := att.AttStatement[stmtPubArea].([]byte) if !present { return "", nil, ErrAttestationFormat.WithDetails("Error retrieving pubArea value") } diff --git a/protocol/attestation_tpm_test.go b/protocol/attestation_tpm_test.go index 412a74f..671994c 100644 --- a/protocol/attestation_tpm_test.go +++ b/protocol/attestation_tpm_test.go @@ -27,7 +27,7 @@ func TestTPMAttestationVerificationSuccess(t *testing.T) { pcc := attestationTestUnpackResponse(t, testAttestationTPMResponses[i]) clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) - attestationType, _, err := verifyTPMFormat(pcc.Response.AttestationObject, clientDataHash[:]) + attestationType, _, err := verifyTPMFormat(pcc.Response.AttestationObject, clientDataHash[:], nil) if err != nil { t.Fatalf("Not valid: %+v", err) } @@ -83,37 +83,37 @@ func TestTPMAttestationVerificationFailAttStatement(t *testing.T) { }, { "TPM Negative Test AttStatement Ver not 2.0", - AttestationObject{AttStatement: map[string]any{"ver": "foo.bar"}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "foo.bar"}}, "WebAuthn only supports TPM 2.0 currently", }, { "TPM Negative Test AttStatement Alg not present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0"}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0"}}, "Error retrieving alg value", }, { "TPM Negative Test AttStatement x5c not present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0)}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0)}}, ErrNotImplemented.Details, }, { "TPM Negative Test AttStatement ecdaaKeyId present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "ecdaaKeyId": []byte{}}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0), stmtX5C: []any{}, stmtECDAAKID: []byte{}}}, ErrNotImplemented.Details, }, { "TPM Negative Test AttStatement sig not present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0), stmtX5C: []any{}}}, "Error retrieving sig value", }, { "TPM Negative Test AttStatement certInfo not present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0), stmtX5C: []any{}, stmtSignature: []byte{}}}, "Error retrieving certInfo value", }, { "TPM Negative Test AttStatement pubArea not present", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0), stmtX5C: []any{}, stmtSignature: []byte{}, stmtCertInfo: []byte{}}}, "Error retrieving pubArea value", }, { @@ -123,12 +123,12 @@ func TestTPMAttestationVerificationFailAttStatement(t *testing.T) { }, { "TPM Negative Test Unsupported Public Key Type", - AttestationObject{AttStatement: map[string]any{"ver": "2.0", "alg": int64(0), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": makeDefaultRSAPublicBytes()}, AuthData: AuthenticatorData{AttData: AttestedCredentialData{CredentialPublicKey: []byte{}}}}, + AttestationObject{AttStatement: map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(0), stmtX5C: []any{}, stmtSignature: []byte{}, stmtCertInfo: []byte{}, stmtPubArea: makeDefaultRSAPublicBytes()}, AuthData: AuthenticatorData{AttData: AttestedCredentialData{CredentialPublicKey: []byte{}}}}, "Unsupported Public Key Type", }, } for _, tt := range tests { - attestationType, _, err := verifyTPMFormat(tt.att, nil) + attestationType, _, err := verifyTPMFormat(tt.att, nil, nil) if tt.wantErr != "" { assert.Contains(t, err.Error(), tt.wantErr) } else { @@ -170,7 +170,7 @@ var ( } ) -var defaultAttStatement = map[string]any{"ver": "2.0", "alg": int64(-257), "x5c": []any{}, "sig": []byte{}, "certInfo": []byte{}, "pubArea": []byte{}} +var defaultAttStatement = map[string]any{stmtVersion: "2.0", stmtAlgorithm: int64(-257), stmtX5C: []any{}, stmtSignature: []byte{}, stmtCertInfo: []byte{}, stmtPubArea: []byte{}} type CredentialPublicKey struct { KeyType int64 `cbor:"1,keyasint" json:"kty"` @@ -354,7 +354,7 @@ func TestTPMAttestationVerificationFailPubArea(t *testing.T) { public = defaultECCPublic } - attStmt["pubArea"], _ = public.Encode() + attStmt[stmtPubArea], _ = public.Encode() att := AttestationObject{ AttStatement: attStmt, AuthData: AuthenticatorData{ @@ -364,7 +364,7 @@ func TestTPMAttestationVerificationFailPubArea(t *testing.T) { }, } - attestationType, _, err := verifyTPMFormat(att, nil) + attestationType, _, err := verifyTPMFormat(att, nil, nil) if tt.wantErr != "" { assert.Contains(t, err.Error(), tt.wantErr) } else { @@ -394,7 +394,7 @@ func TestTPMAttestationVerificationFailCertInfo(t *testing.T) { public := defaultRSAPublic public.RSAParameters.ExponentRaw = uint32(rsaKey.E) public.RSAParameters.ModulusRaw = rsaKey.N.Bytes() - attStmt["pubArea"], _ = public.Encode() + attStmt[stmtPubArea], _ = public.Encode() rpk, _ := webauthncbor.Marshal(r) att := AttestationObject{ AttStatement: attStmt, @@ -452,8 +452,8 @@ func TestTPMAttestationVerificationFailCertInfo(t *testing.T) { t.Fatal(err) } - att.AttStatement["certInfo"] = certInfo - attestationType, _, err := verifyTPMFormat(att, nil) + att.AttStatement[stmtCertInfo] = certInfo + attestationType, _, err := verifyTPMFormat(att, nil, nil) if tt.wantErr != "" { assert.Contains(t, err.Error(), tt.wantErr) @@ -509,7 +509,7 @@ func TestTPMAttestationVerificationFailX5c(t *testing.T) { public.RSAParameters.ExponentRaw = uint32(rsaKey.E) public.RSAParameters.ModulusRaw = rsaKey.N.Bytes() pubBytes, _ := public.Encode() - attStmt["pubArea"] = pubBytes + attStmt[stmtPubArea] = pubBytes rpk, _ := webauthncbor.Marshal(r) att := AttestationObject{ AttStatement: attStmt, @@ -542,7 +542,7 @@ func TestTPMAttestationVerificationFailX5c(t *testing.T) { }, ExtraData: extraData, } - attStmt["certInfo"], _ = certInfo.Encode() + attStmt[stmtCertInfo], _ = certInfo.Encode() makeX5c := func(b []byte) []any { q := make([]any, 1) @@ -569,8 +569,8 @@ func TestTPMAttestationVerificationFailX5c(t *testing.T) { } for _, tt := range tests { - att.AttStatement["x5c"] = tt.x5c - attestationType, _, err := verifyTPMFormat(att, nil) + att.AttStatement[stmtX5C] = tt.x5c + attestationType, _, err := verifyTPMFormat(att, nil, nil) if tt.wantErr != "" { assert.Contains(t, err.Error(), tt.wantErr) diff --git a/protocol/attestation_u2f.go b/protocol/attestation_u2f.go index 0689d42..211aab7 100644 --- a/protocol/attestation_u2f.go +++ b/protocol/attestation_u2f.go @@ -16,7 +16,7 @@ func init() { } // verifyU2FFormat - Follows verification steps set out by https://www.w3.org/TR/webauthn/#fido-u2f-attestation -func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []any, error) { +func verifyU2FFormat(att AttestationObject, clientDataHash []byte, _ metadata.Provider) (string, []any, error) { if !bytes.Equal(att.AuthData.AttData.AAGUID, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}) { return "", nil, ErrUnsupportedAlgorithm.WithDetails("U2F attestation format AAGUID not set to 0x00") } @@ -40,7 +40,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []an // } // Check for "x5c" which is a single element array containing the attestation certificate in X.509 format. - x5c, present := att.AttStatement["x5c"].([]any) + x5c, present := att.AttStatement[stmtX5C].([]any) if !present { return "", nil, ErrAttestationFormat.WithDetails("Missing properly formatted x5c data") } @@ -48,7 +48,7 @@ func verifyU2FFormat(att AttestationObject, clientDataHash []byte) (string, []an // Check for "sig" which is The attestation signature. The signature was calculated over the (raw) U2F // registration response message https://www.w3.org/TR/webauthn/#biblio-fido-u2f-message-formats] // received by the client from the authenticator. - signature, present := att.AttStatement["sig"].([]byte) + signature, present := att.AttStatement[stmtSignature].([]byte) if !present { return "", nil, ErrAttestationFormat.WithDetails("Missing sig data") } diff --git a/protocol/attestation_u2f_test.go b/protocol/attestation_u2f_test.go index 04dc618..b05bd23 100644 --- a/protocol/attestation_u2f_test.go +++ b/protocol/attestation_u2f_test.go @@ -37,7 +37,7 @@ func TestVerifyU2FFormat(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, _, err := verifyU2FFormat(tt.args.att, tt.args.clientDataHash) + got, _, err := verifyU2FFormat(tt.args.att, tt.args.clientDataHash, nil) if (err != nil) != tt.wantErr { t.Errorf("verifyU2FFormat() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/protocol/const.go b/protocol/const.go new file mode 100644 index 0000000..a1560f2 --- /dev/null +++ b/protocol/const.go @@ -0,0 +1,11 @@ +package protocol + +const ( + stmtX5C = "x5c" + stmtSignature = "sig" + stmtAlgorithm = "alg" + stmtVersion = "ver" + stmtECDAAKID = "ecdaaKeyId" + stmtCertInfo = "certInfo" + stmtPubArea = "pubArea" +) diff --git a/protocol/credential.go b/protocol/credential.go index e8eeb2a..64c8928 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -3,9 +3,10 @@ package protocol import ( "crypto/sha256" "encoding/base64" - "github.com/go-webauthn/webauthn/metadata" "io" "net/http" + + "github.com/go-webauthn/webauthn/metadata" ) // Credential is the basic credential type from the Credential Management specification that is inherited by WebAuthn's diff --git a/protocol/webauthncose/webauthncose.go b/protocol/webauthncose/webauthncose.go index 76a0aad..eb1f0d7 100644 --- a/protocol/webauthncose/webauthncose.go +++ b/protocol/webauthncose/webauthncose.go @@ -35,6 +35,7 @@ type PublicKeyData struct { // A COSEAlgorithmIdentifier for the algorithm used to derive the key signature. Algorithm int64 `cbor:"3,keyasint" json:"alg"` } + type EC2PublicKeyData struct { PublicKeyData diff --git a/webauthn/types.go b/webauthn/types.go index cd53db6..58415ab 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -2,10 +2,10 @@ package webauthn import ( "fmt" - "github.com/go-webauthn/webauthn/metadata" "net/url" "time" + "github.com/go-webauthn/webauthn/metadata" "github.com/go-webauthn/webauthn/protocol" ) From efccbade90bcadd417c1c3a476fe4abd4d824b8b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 18 May 2024 22:24:58 +1000 Subject: [PATCH 06/24] temp --- metadata/const.go | 22 ++-- metadata/decode.go | 172 +++++-------------------------- metadata/metadata.go | 4 +- metadata/metadata_test.go | 54 +++++----- metadata/types.go | 15 --- protocol/attestation.go | 60 ++++++----- protocol/attestation_test.go | 49 ++------- protocol/attestation_tpm_test.go | 2 + protocol/base64_test.go | 17 +-- protocol/client_test.go | 17 +-- 10 files changed, 127 insertions(+), 285 deletions(-) diff --git a/metadata/const.go b/metadata/const.go index 8ef7f59..97a5ad4 100644 --- a/metadata/const.go +++ b/metadata/const.go @@ -14,14 +14,22 @@ const ( ExampleMDSRoot = "MIIGGTCCBAGgAwIBAgIUdT9qLX0sVMRe8l0sLmHd3mZovQ0wDQYJKoZIhvcNAQELBQAwgZsxHzAdBgNVBAMMFkVYQU1QTEUgTURTMyBURVNUIFJPT1QxIjAgBgkqhkiG9w0BCQEWE2V4YW1wbGVAZXhhbXBsZS5jb20xFDASBgNVBAoMC0V4YW1wbGUgT1JHMRAwDgYDVQQLDAdFeGFtcGxlMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTVkxEjAQBgNVBAcMCVdha2VmaWVsZDAeFw0yMTA0MTkxMTM1MDdaFw00ODA5MDQxMTM1MDdaMIGbMR8wHQYDVQQDDBZFWEFNUExFIE1EUzMgVEVTVCBST09UMSIwIAYJKoZIhvcNAQkBFhNleGFtcGxlQGV4YW1wbGUuY29tMRQwEgYDVQQKDAtFeGFtcGxlIE9SRzEQMA4GA1UECwwHRXhhbXBsZTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk1ZMRIwEAYDVQQHDAlXYWtlZmllbGQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDjF5wyEWuhwDHsZosGdGFTCcI677rW881vV+UfW38J+K2ioFFNeGVsxbcebK6AVOiCDPFj0974IpeD9SFOhwAHoDu/LCfXdQWp8ZgQ91ULYWoW8o7NNSp01nbN9zmaO6/xKNCa0bzjmXoGqglqnP1AtRcWYvXOSKZy1rcPeDv4Dhcpdp6W72fBw0eWIqOhsrItuY2/N8ItBPiG03EX72nACq4nZJ/nAIcUbER8STSFPPzvE97TvShsi1FD8aO6l1WkR/QkreAGjMI++GbB2Qc1nN9Y/VEDbMDhQtxXQRdpFwubTjejkN9hKOtF3B71YrwIrng3V9RoPMFdapWMzSlI+WWHog0oTj1PqwJDDg7+z1I6vSDeVWAMKr9mq1w1OGNzgBopIjd9lRWkRtt2kQSPX9XxqS4E1gDDr8MKbpM3JuubQtNCg9D7Ljvbz6vwvUrbPHH+oREvucsp0PZ5PpizloepGIcLFxDQqCulGY2n7Ahl0JOFXJqOFCaK3TWHwBvZsaY5DgBuUvdUrwtgZNg2eg2omWXEepiVFQn3Fvj43Wh2npPMgIe5P0rwncXvROxaczd4rtajKS1ucoB9b9iKqM2+M1y/FDIgVf1fWEHwK7YdzxMlgOeLdeV/kqRU5PEUlLU9a2EwdOErrPbPKZmIfbs/L4B3k4zejMDH3Y+ZwIDAQABo1MwUTAdBgNVHQ4EFgQU8sWwq1TrurK7xMTwO1dKfeJBbCMwHwYDVR0jBBgwFoAU8sWwq1TrurK7xMTwO1dKfeJBbCMwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAFw6M1PiIfCPIBQ5EBUPNmRvRFuDpolOmDofnf/+mv63LqwQZAdo/W8tzZ9kOFhq24SiLw0H7fsdG/jeREXiIZMNoW/rA6Uac8sU+FYF7Q+qp6CQLlSQbDcpVMifTQjcBk2xh+aLK9SrrXBqnTAhwS+offGtAW8DpoLuH4tAcQmIjlgMlN65jnELCuqNR/wpA+zch8LZW8saQ2cwRCwdr8mAzZoLbsDSVCHxQF3/kQjPT7Nao1q2iWcY3OYcRmKrieHDP67yeLUbVmetfZis2d6ZlkqHLB4ZW1xX4otsEFkuTJA3HWDRsNyhTwx1YoCLsYut5Zp0myqPNBq28w6qGMyyoJN0Z4RzMEO3R6i/MQNfhK55/8O2HciM6xb5t/aBSuHPKlBDrFWhpRnKYkaNtlUo35qV5IbKGKau3SdZdSRciaXUd/p81YmoF01UlhhMz/Rqr1k2gyA0a9tF8+awCeanYt5izl8YO0FlrOU1SQ5UQw4szqqZqbrf4e8fRuU2TXNx4zk+ImE7WRB44f6mSD746ZCBRogZ/SA5jUBu+OPe4/sEtERWRcQD+fXgce9ZEN0+peyJIKAsl5Rm2Bmgyg5IoyWwSG5W+WekGyEokpslou2Yc6EjUj5ndZWz5EiHAiQ74hNfDoCZIxVVLU3Qbp8a0S1bmsoT2JOsspIbtZUg=" ) -var ( - // Conformance indicates if test metadata is currently being used - Conformance = false - - MDSRoot = ProductionMDSRoot -) - const ( HeaderX509URI = "x5u" HeaderX509Certificate = "x5c" ) + +var ( + errIntermediateCertRevoked = &MetadataError{ + Type: "intermediate_revoked", + Details: "Intermediate certificate is on issuers revocation list", + } + errLeafCertRevoked = &MetadataError{ + Type: "leaf_revoked", + Details: "Leaf certificate is on issuers revocation list", + } + errCRLUnavailable = &MetadataError{ + Type: "crl_unavailable", + Details: "Certificate revocation list is unavailable", + } +) diff --git a/metadata/decode.go b/metadata/decode.go index cc6fd80..293ea9f 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -16,7 +16,7 @@ import ( ) // NewDecoder returns a new metadata decoder. -func NewDecoder(opts ...DecoderOption) (decoder *Decoder) { +func NewDecoder(opts ...DecoderOption) (decoder *Decoder, err error) { decoder = &Decoder{ client: &http.Client{}, parser: jwt.NewParser(), @@ -24,16 +24,23 @@ func NewDecoder(opts ...DecoderOption) (decoder *Decoder) { } for _, opt := range opts { - opt(decoder) + if err = opt(decoder); err != nil { + return nil, err + } + } + + if decoder.root == "" { + decoder.root = ProductionMDSRoot } - return decoder + return decoder, nil } type Decoder struct { client *http.Client parser *jwt.Parser hook mapstructure.DecodeHookFunc + root string ignoreEntryParsingErrors bool } @@ -102,13 +109,13 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, e if x5c, ok = token.Header[HeaderX509Certificate].([]any); !ok { // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. - chain[0] = MDSRoot + chain[0] = d.root } else { chain = x5c } // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. - if valid, err = validateChain(chain); !valid || err != nil { + if valid, err = validateChain(d.root, chain); !valid || err != nil { return nil, err } @@ -155,65 +162,10 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, e return payload, nil } -func unmarshalMDSBLOB(body []byte) (MetadataBLOBPayloadJSON, error) { - var payload MetadataBLOBPayloadJSON - - token, err := jwt.Parse(string(body), func(token *jwt.Token) (any, error) { - // 2. If the x5u attribute is present in the JWT Header, then - if _, ok := token.Header[HeaderX509URI].([]any); ok { - // never seen an x5u here, although it is in the spec - return nil, errors.New("x5u encountered in header of metadata TOC payload") - } - var chain []any - // 3. If the x5u attribute is missing, the chain should be retrieved from the x5c attribute. - - if x5c, ok := token.Header[HeaderX509Certificate].([]any); !ok { - // If that attribute is missing as well, Metadata TOC signing trust anchor is considered the TOC signing certificate chain. - chain[0] = MDSRoot - } else { - chain = x5c - } - - // The certificate chain MUST be verified to properly chain to the metadata TOC signing trust anchor. - valid, err := validateChain(chain) - if !valid || err != nil { - return nil, err - } - - // Chain validated, extract the TOC signing certificate from the chain. Create a buffer large enough to hold the - // certificate bytes. - o := make([]byte, base64.StdEncoding.DecodedLen(len(chain[0].(string)))) - - // base64 decode the certificate into the buffer. - n, err := base64.StdEncoding.Decode(o, []byte(chain[0].(string))) - if err != nil { - return nil, err - } - - // Parse the certificate from the buffer. - cert, err := x509.ParseCertificate(o[:n]) - if err != nil { - return nil, err - } - - // 4. Verify the signature of the Metadata TOC object using the TOC signing certificate chain - // jwt.Parse() uses the TOC signing certificate public key internally to verify the signature. - return cert.PublicKey, err - }) - - if err != nil { - return payload, err - } - - err = mapstructure.Decode(token.Claims, &payload) - - return payload, err -} - -func validateChain(chain []any) (bool, error) { - oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(MDSRoot))) +func validateChain(root string, chain []any) (bool, error) { + oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(root))) - nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(MDSRoot)) + nRoot, err := base64.StdEncoding.Decode(oRoot, []byte(root)) if err != nil { return false, err } @@ -280,85 +232,7 @@ func validateChain(chain []any) (bool, error) { return err == nil, err } -const x = ` -// 95e4d58c-056e-4a65-866d-f5a69659e880 / TruU Windows Authenticator: ------BEGIN CERTIFICATE----- -LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTekNDQWZLZ0F3SUJBZ0lV -VzNYSzh5eXdiQVdsaWdsaXhJRjYzZHZxWXk4d0NnWUlLb1pJemowRUF3SXcKZkRF -TE1Ba0dBMVVFQmhNQ1ZWTXhFVEFQQmdOVkJBZ01DRU52Ykc5eVlXUnZNUTh3RFFZ -RFZRUUhEQVpFWlc1MgpaWEl4RXpBUkJnTlZCQW9NQ2xSeWRWVXNJRWx1WXk0eElq -QWdCZ05WQkFzTUdVRjFkR2hsYm5ScFkyRjBiM0lnClFYUjBaWE4wWVhScGIyNHhF -REFPQmdOVkJBTU1CM1J5ZFhVdVlXa3dJQmNOTWpNeE1UQXpNakF6TmpVeFdoZ1AK -TWpBMU16RXdNall5TURNMk5URmFNSHd4Q3pBSkJnTlZCQVlUQWxWVE1SRXdEd1lE -VlFRSURBaERiMnh2Y21GawpiekVQTUEwR0ExVUVCd3dHUkdWdWRtVnlNUk13RVFZ -RFZRUUtEQXBVY25WVkxDQkpibU11TVNJd0lBWURWUVFMCkRCbEJkWFJvWlc1MGFX -TmhkRzl5SUVGMGRHVnpkR0YwYVc5dU1SQXdEZ1lEVlFRRERBZDBjblYxTG1GcE1G -a3cKRXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVOQXZidGNjTXI3ai9T -UldtcUlFWlRSV05KeWo2bXNZcgo1bEdlQWdkU0d5QzlPMDM1NlJJZWN1YVZpT3F6 -MER4Z1MxZi81S1BiWnAxdDB5RDJmVlJYOTZOUU1FNHdIUVlEClZSME9CQllFRkE1 -dEwxMGc4OHQycVhsUGxoSVNJMmRJemxhVk1COEdBMVVkSXdRWU1CYUFGQTV0TDEw -Zzg4dDIKcVhsUGxoSVNJMmRJemxhVk1Bd0dBMVVkRXdFQi93UUNNQUF3Q2dZSUtv -Wkl6ajBFQXdJRFJ3QXdSQUlnWGZ1dgpqc3ArNHY1aUdPcW5nVWdPZzFobWJnRlBG -TWdJanlXeENLcXcvZDhDSUZpbUxOWExESXdBK29JYlAxeU9mcUU4CnhrNnE3LzRM -V09WWWtSQUxvQkMyCi0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= ------END CERTIFICATE----- - -// 4c0cf95d-2f40-43b5-ba42-4c83a11c04ba / Feitian BioPass FIDO2 Pro Authenticator ------BEGIN CERTIFICATE----- -MIIB2TCCAX6gAwIBAgIQFQNKW+7zbg/7d+lTyrIWwDAKBggqhkjOPQQDAjBLMQsw -CQYDVQQGEwJVUzEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9sb2dpZXMxHTAbBgNV -BAMMFEZlaXRpYW4gRklETyBSb290IENBMCAXDTIyMDYwODAwMDAwMFoYDzIwNTIw -NjA3MjM1OTU5WjBLMQswCQYDVQQGEwJVUzEdMBsGA1UECgwURmVpdGlhbiBUZWNo -bm9sb2dpZXMxHTAbBgNVBAMMFEZlaXRpYW4gRklETyBSb290IENBMFkwEwYHKoZI -zj0CAQYIKoZIzj0DAQcDQgAEsFYEEhiJuqqnMgQjSiivBjV7DGCTf4XBBH/B7uvZ -sKxXShF0L8uDISWUvcExixRs6gB3oldSrjox6L8T94NOzqNCMEAwHQYDVR0OBBYE -FEu9hyYRrRyJzwRYvnDSCIxrFiO3MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ -BAQDAgEGMAoGCCqGSM49BAMCA0kAMEYCIQCB0NFQSN0z4lWz/yc36ewrTCzttK/q -FvlaPOKh+T1o6wIhAP0oKKA+cicsDy3Y3n+VlP8eB3PBzMkhvW/9ISXCw+VBMIIB -2DCCAX6gAwIBAgIQBTmk3ZwilFXjsZywHDnMgDAKBggqhkjOPQQDAjBLMQswCQYD -VQQGEwJDTjEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9sb2dpZXMxHTAbBgNVBAMM -FEZlaXRpYW4gRklETyBSb290IENBMCAXDTIyMDYwODAwMDAwMFoYDzIwNTIwNjA3 -MjM1OTU5WjBLMQswCQYDVQQGEwJDTjEdMBsGA1UECgwURmVpdGlhbiBUZWNobm9s -b2dpZXMxHTAbBgNVBAMMFEZlaXRpYW4gRklETyBSb290IENBMFkwEwYHKoZIzj0C -AQYIKoZIzj0DAQcDQgAEnfAKbjvMX1Ey1b6k+WQQdNVMt9JgGWyJ3PvM4BSK5XqT -fo++0oAj/4tnwyIL0HFBR9St+ktjqSXDfjiXAurs86NCMEAwHQYDVR0OBBYEFNGh -mE2Bf8O5a/YHZ71QEv6QRfFUMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD -AgEGMAoGCCqGSM49BAMCA0gAMEUCIByRz4OAlRZ9Hz9KV7g2QNtC0C8JxH/xLJY8 -FZEmtJ3sAiEAsreT0+eNkNcUjI9h5OPCoH6NmsOkgvEABJZrF07ADkY= ------END CERTIFICATE----- - -// ca87cb70-4c1b-4579-a8e8-4efdd7c007e0 / FIDO Alliance TruU Sample FIDO2 Authenticator ------BEGIN CERTIFICATE----- -LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURYekNDQWtlZ0F3SUJBZ0lM -QkFBQUFBQUJJVmhUQ0tJd0RRWUpLb1pJaHZjTkFRRUxCUUF3VERFZ01CNEcKQTFV -RUN4TVhSMnh2WW1Gc1UybG5iaUJTYjI5MElFTkJJQzBnVWpNeEV6QVJCZ05WQkFv -VENrZHNiMkpoYkZOcApaMjR4RXpBUkJnTlZCQU1UQ2tkc2IySmhiRk5wWjI0d0ho -Y05NRGt3TXpFNE1UQXdNREF3V2hjTk1qa3dNekU0Ck1UQXdNREF3V2pCTU1TQXdI -Z1lEVlFRTEV4ZEhiRzlpWVd4VGFXZHVJRkp2YjNRZ1EwRWdMU0JTTXpFVE1CRUcK -QTFVRUNoTUtSMnh2WW1Gc1UybG5iakVUTUJFR0ExVUVBeE1LUjJ4dlltRnNVMmxu -YmpDQ0FTSXdEUVlKS29aSQpodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQU13 -bGRwQjVCbmdpRnZYQWc3YUV5aWllL1FWMkVjV3RpSEw4ClJnSkR4N0tLblFSZkpN -c3VTK0ZnZ2tiaFVxc01nVWR3Yk4xazBldjFMS01QZ2owTUs2NlgxN1lVaGhCNXV6 -c1QKZ0hlTUNPRkowbXBpTHg5ZStwWm8zNGtubFRpZkJ0Yyt5Y3NtV1ExejNyREk2 -U1lPZ3hYRzcxdUwwZ1JneWttbQpLUFpwTy9iTHlDaVI1WjJLWVZjM3JIUVUzSFRn -T3U1eUx5NmMrOUM3di9VOUFPRUdNK2lDSzY1VHBqb1djNHpkClFRNGdPc0MwcDZI -cHNrK1FMakpnNlZmTHVRU1NhR2psT0NaZ2RiS2ZkLytSRk8rdUlFbjhyVUFWU05F -Q01XRVoKWHJpWDc2MTN0MlNhZXI5ZndSUHZtMkw3RFd6Z1ZHa1dxUVBhYnVtRGsz -RjJ4bW1GZ2hjQ0F3RUFBYU5DTUVBdwpEZ1lEVlIwUEFRSC9CQVFEQWdFR01BOEdB -MVVkRXdFQi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZJL3dTMytvCkxrVWtyazFR -K21PYWk5N2kzUnU4TUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFCTFFOdkFVS3Ir -eUF6djk1WlUKUlVtN2xnQUpRYXl6RTRhR0tBY3p5bXZtZExtNkFDMnVwQXJUOWZI -eEQ0cS9jMmRLZzhkRWUzamdyMjVzYndNcApqak01UmNPTzVMbFhiS3I4RXBic1U4 -WXQ1Q1JzdVpSais5eFRhR2RXUG9PNHp6VWh3OGxvL3M3YXdsT3F6SkNLCjZmQmRS -b3lWM1hwWUtCb3ZIZDdOQURkQmorMUViZGRUS0pkKzgyY0VIaFhYaXBhMDA5NU1K -NlJNRzNOemR2UVgKbWNJZmVnN2pMUWl0Q2h3cy96eXJWUTRQa1g0MjY4TlhTYjdo -TGkxOFlJdkRRVkVUSTUzTzl6SnJsQUdvbWVjcwpNeDg2T3lYU2hrRE9PeXlHZU1s -aEx4UzY3dHRWYjkrRTdnVUpUYjBvMkhMTzAySlFaUjdya3BlRE1kbXp0Y3BICldE -OWYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ== ------END CERTIFICATE----- -` - -func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, err error) { +func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err error) { var n int raw := make([]byte, base64.StdEncoding.DecodedLen(len(value))) @@ -374,12 +248,22 @@ func ParseMetadataX509Certificate(value string) (certificate *x509.Certificate, return certificate, nil } -type DecoderOption func(decoder *Decoder) +type DecoderOption func(decoder *Decoder) (err error) // WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for // these entries will exist as an unparsed entry. func WithIgnoreEntryParsingErrors() DecoderOption { - return func(decoder *Decoder) { + return func(decoder *Decoder) (err error) { decoder.ignoreEntryParsingErrors = true + + return nil + } +} + +func WithRootCertificate(value string) DecoderOption { + return func(decoder *Decoder) (err error) { + decoder.root = value + + return nil } } diff --git a/metadata/metadata.go b/metadata/metadata.go index 8e90d2a..9cbc9f6 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -354,7 +354,7 @@ func (j MetadataStatementJSON) Parse() (statement MetadataStatement, err error) certificates := make([]*x509.Certificate, n) for i := 0; i < n; i++ { - if certificates[i], err = ParseMetadataX509Certificate(j.AttestationRootCertificates[i]); err != nil { + if certificates[i], err = mdsParseX509Certificate(j.AttestationRootCertificates[i]); err != nil { return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing attestation root certificate %d value: %w", j.Description, i, err) } } @@ -515,7 +515,7 @@ func (j StatusReportJSON) Parse() (report StatusReport, err error) { var certificate *x509.Certificate if len(j.Certificate) != 0 { - if certificate, err = ParseMetadataX509Certificate(j.Certificate); err != nil { + if certificate, err = mdsParseX509Certificate(j.Certificate); err != nil { return report, fmt.Errorf("error occurred parsing certificate value: %w", err) } } diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index efce25b..e55878e 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -3,6 +3,7 @@ package metadata import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -17,7 +18,8 @@ import ( ) func TestProductionMetadataTOCParsing(t *testing.T) { - decoder := NewDecoder(WithIgnoreEntryParsingErrors()) + decoder, err := NewDecoder(WithIgnoreEntryParsingErrors()) + require.NoError(t, err) client := &http.Client{} @@ -38,9 +40,7 @@ func TestProductionMetadataTOCParsing(t *testing.T) { } func TestConformanceMetadataTOCParsing(t *testing.T) { - MDSRoot = ConformanceMDSRoot - Conformance = true - httpClient := &http.Client{ + client := &http.Client{ Timeout: time.Second * 30, } @@ -74,24 +74,27 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { }, } - endpoints, err := getEndpoints(httpClient) - if err != nil { - t.Fatal(err) - } + endpoints, err := getEndpoints(client) + require.NoError(t, err) - decoder := NewDecoder() + decoder, err := NewDecoder(WithRootCertificate(ConformanceMDSRoot)) + + require.NoError(t, err) metadata := make(map[uuid.UUID]MetadataBLOBPayloadEntryJSON) + var ( + res *http.Response + blob *MetadataBLOBPayloadJSON + me *MetadataError + ) + for _, endpoint := range endpoints { - res, err := httpClient.Get(endpoint) - if err != nil { - t.Fatal(err) - } + res, err = client.Get(endpoint) + require.NoError(t, err) - blob, err := decoder.Decode(res.Body) - if err != nil { - if me, ok := err.(*MetadataError); ok { + if blob, err = decoder.Decode(res.Body); err != nil { + if errors.As(err, &me) { t.Log(me.Details) } } @@ -106,10 +109,9 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - statement, err := getTestMetadata(tc.name, httpClient) - if err != nil { - t.Fatal(err) - } + statement, err := getTestMetadata(tc.name, client) + require.NoError(t, err) + aaguid, _ := uuid.Parse(statement.AaGUID) if meta, ok := metadata[aaguid]; ok { pass := true @@ -125,11 +127,7 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { _, err := meta.Parse() assert.NoError(t, err, "Failed to parse metadata") } else { - if !tc.pass { - t.Logf("Metadata for aaguid %s not found in test metadata", statement.AaGUID) - } else { - t.Fail() - } + assert.False(t, tc.pass) } }) } @@ -140,11 +138,11 @@ const ( ) func TestExampleMetadataTOCParsing(t *testing.T) { - MDSRoot = ExampleMDSRoot - exampleMetadataBLOBBytes := bytes.NewBufferString(exampleMetadataBLOB) - decoder := NewDecoder(WithIgnoreEntryParsingErrors()) + decoder, err := NewDecoder(WithIgnoreEntryParsingErrors(), WithRootCertificate(ExampleMDSRoot)) + + require.NoError(t, err) payload, err := decoder.DecodeBytes(exampleMetadataBLOBBytes.Bytes()) require.NoError(t, err) diff --git a/metadata/types.go b/metadata/types.go index 7b56aa0..e0e47c2 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -227,21 +227,6 @@ type MetadataError struct { DevInfo string `json:"debug"` } -var ( - errIntermediateCertRevoked = &MetadataError{ - Type: "intermediate_revoked", - Details: "Intermediate certificate is on issuers revocation list", - } - errLeafCertRevoked = &MetadataError{ - Type: "leaf_revoked", - Details: "Leaf certificate is on issuers revocation list", - } - errCRLUnavailable = &MetadataError{ - Type: "crl_unavailable", - Details: "Certificate revocation list is unavailable", - } -) - func (err *MetadataError) Error() string { return err.Details } diff --git a/protocol/attestation.go b/protocol/attestation.go index f77c5cb..9964536 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -121,11 +121,11 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon // // Steps 9 through 12 are verified against the auth data. These steps are identical to 11 through 14 for assertion so we // handle them with AuthData. -func (attestationObject *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool, mds metadata.Provider) error { +func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool, mds metadata.Provider) error { rpIDHash := sha256.Sum256([]byte(relyingPartyID)) // Begin Step 9 through 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP. - authDataVerificationError := attestationObject.AuthData.Verify(rpIDHash[:], nil, verificationRequired) + authDataVerificationError := a.AuthData.Verify(rpIDHash[:], nil, verificationRequired) if authDataVerificationError != nil { return authDataVerificationError } @@ -142,23 +142,23 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client // But first let's make sure attestation is present. If it isn't, we don't need to handle // any of the following steps - if AttestationFormat(attestationObject.Format) == AttestationFormatNone { - if len(attestationObject.AttStatement) != 0 { + if AttestationFormat(a.Format) == AttestationFormatNone { + if len(a.AttStatement) != 0 { return ErrAttestationFormat.WithInfo("Attestation format none with attestation present") } return nil } - formatHandler, valid := attestationRegistry[AttestationFormat(attestationObject.Format)] + formatHandler, valid := attestationRegistry[AttestationFormat(a.Format)] if !valid { - return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", attestationObject.Format)) + return ErrAttestationFormat.WithInfo(fmt.Sprintf("Attestation format %s is unsupported", a.Format)) } // Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using // the attestation statement format fmt’s verification procedure given attStmt, authData and the hash of the serialized // client data computed in step 7. - attestationType, x5cs, err := formatHandler(*attestationObject, clientDataHash, mds) + attestationType, x5cs, err := formatHandler(*a, clientDataHash, mds) if err != nil { return err.(*Error).WithInfo(attestationType) } @@ -168,7 +168,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client entry *metadata.MetadataBLOBPayloadEntry ) - if aaguid, err = uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID); err != nil { + if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { return err } @@ -196,29 +196,35 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client } } - if x5cs != nil { - var ( - x5c *x509.Certificate - raw []byte - ok bool - ) + if x5cs == nil { + return nil + } - if raw, ok = x5cs[0].([]byte); !ok { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") - } + var ( + x5c *x509.Certificate + raw []byte + ok bool + ) - if x5c, err = x509.ParseCertificate(raw); err != nil { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c") - } + if len(x5cs) == 0 { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo("The attestation had no certificates") + } + + if raw, ok = x5cs[0].([]byte); !ok { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) + } - if x5c.Subject.CommonName != x5c.Issuer.CommonName { - if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { - return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") - } + if x5c, err = x509.ParseCertificate(raw); err != nil { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) + } + + if x5c.Subject.CommonName != x5c.Issuer.CommonName { + if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { + return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + } - if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) - } + if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) } } diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index cbb2d47..9aa5ae3 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -5,69 +5,45 @@ import ( "encoding/json" "fmt" "testing" + + "github.com/stretchr/testify/require" ) func TestAttestationVerify(t *testing.T) { - //assert.NoError(t, metadata.PopulateMetadata(metadata.ProductionMDSURL, true)) - for i := range testAttestationOptions { t.Run(fmt.Sprintf("Running test %d", i), func(t *testing.T) { options := CredentialCreation{} - if err := json.Unmarshal([]byte(testAttestationOptions[i]), &options); err != nil { - t.Fatal(err) - } + + require.NoError(t, json.Unmarshal([]byte(testAttestationOptions[i]), &options)) ccr := CredentialCreationResponse{} - if err := json.Unmarshal([]byte(testAttestationResponses[i]), &ccr); err != nil { - t.Fatal(err) - } + require.NoError(t, json.Unmarshal([]byte(testAttestationResponses[i]), &ccr)) var pcc ParsedCredentialCreationData pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults pcc.Raw = ccr parsedAttestationResponse, err := ccr.AttestationResponse.Parse() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pcc.Response = *parsedAttestationResponse - // Test Base Verification - err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode, nil) - if err != nil { - t.Fatalf("Not valid: %+v (%s)", err, err.(*Error).DevInfo) - } + require.NoError(t, pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode, nil)) }) - - fmt.Println("Done") - } -} - -func attestationTestUnpackRequest(t *testing.T, request string) CredentialCreation { - options := CredentialCreation{} - - if err := json.Unmarshal([]byte(request), &options); err != nil { - t.Fatal(err) } - - return options } func attestationTestUnpackResponse(t *testing.T, response string) (pcc ParsedCredentialCreationData) { ccr := CredentialCreationResponse{} - if err := json.Unmarshal([]byte(response), &ccr); err != nil { - t.Fatal(err) - } + + require.NoError(t, json.Unmarshal([]byte(response), &ccr)) pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults pcc.Raw = ccr parsedAttestationResponse, err := ccr.AttestationResponse.Parse() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) pcc.Response = *parsedAttestationResponse @@ -75,7 +51,6 @@ func attestationTestUnpackResponse(t *testing.T, response string) (pcc ParsedCre } func TestPackedAttestationVerification(t *testing.T) { - t.Run("Testing Self Packed", func(t *testing.T) { pcc := attestationTestUnpackResponse(t, testAttestationResponses[0]) @@ -83,9 +58,7 @@ func TestPackedAttestationVerification(t *testing.T) { clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) _, _, err := verifyPackedFormat(pcc.Response.AttestationObject, clientDataHash[:], nil) - if err != nil { - t.Fatalf("Not valid: %+v", err) - } + require.NoError(t, err) }) } diff --git a/protocol/attestation_tpm_test.go b/protocol/attestation_tpm_test.go index 671994c..3f33763 100644 --- a/protocol/attestation_tpm_test.go +++ b/protocol/attestation_tpm_test.go @@ -11,6 +11,7 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/binary" + "github.com/stretchr/testify/require" "math/big" "testing" @@ -28,6 +29,7 @@ func TestTPMAttestationVerificationSuccess(t *testing.T) { clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) attestationType, _, err := verifyTPMFormat(pcc.Response.AttestationObject, clientDataHash[:], nil) + require.NoError(t, err) if err != nil { t.Fatalf("Not valid: %+v", err) } diff --git a/protocol/base64_test.go b/protocol/base64_test.go index 8b91897..2540a54 100644 --- a/protocol/base64_test.go +++ b/protocol/base64_test.go @@ -1,10 +1,11 @@ package protocol import ( - "bytes" "encoding/base64" "encoding/json" "fmt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "strings" "testing" ) @@ -41,17 +42,9 @@ func TestBase64UnmarshalJSON(t *testing.T) { t.Logf("%s\n", raw) - err := json.NewDecoder(strings.NewReader(raw)).Decode(&got) - if err != nil { - t.Fatalf("error decoding JSON: %v", err) - } + require.NoError(t, json.NewDecoder(strings.NewReader(raw)).Decode(&got)) - if !bytes.Equal(test.expectedTestData.EncodedData, got.EncodedData) { - t.Fatalf("invalid URLEncodedBase64 data received: expected %s got %s", test.expectedTestData.EncodedData, got.EncodedData) - } - - if test.expectedTestData.StringData != got.StringData { - t.Fatalf("invalid string data received: expected %s got %s", test.expectedTestData.StringData, got.StringData) - } + assert.Equal(t, test.expectedTestData.EncodedData, got.EncodedData) + assert.Equal(t, test.expectedTestData.StringData, got.StringData) } } diff --git a/protocol/client_test.go b/protocol/client_test.go index 1a2df9e..5ab5049 100644 --- a/protocol/client_test.go +++ b/protocol/client_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func setupCollectedClientData(challenge URLEncodedBase64, origin string) *CollectedClientData { @@ -18,17 +19,13 @@ func setupCollectedClientData(challenge URLEncodedBase64, origin string) *Collec func TestVerifyCollectedClientData(t *testing.T) { newChallenge, err := CreateChallenge() - if err != nil { - t.Fatalf("error creating challenge: %s", err) - } + require.NoError(t, err) ccd := setupCollectedClientData(newChallenge, "http://example.com") var storedChallenge = newChallenge - if err = ccd.Verify(storedChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode); err != nil { - t.Fatalf("error verifying challenge: expected %#v got %#v", ccd.Challenge, storedChallenge) - } + require.NoError(t, ccd.Verify(storedChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode)) } func TestVerifyCollectedClientDataIncorrectChallenge(t *testing.T) { @@ -40,13 +37,9 @@ func TestVerifyCollectedClientDataIncorrectChallenge(t *testing.T) { ccd := setupCollectedClientData(newChallenge, "http://example.com") bogusChallenge, err := CreateChallenge() - if err != nil { - t.Fatalf("error creating challenge: %s", err) - } + require.NoError(t, err) - if err = ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode); err == nil { - t.Fatalf("error expected but not received. expected %#v got %#v", ccd.Challenge, bogusChallenge) - } + assert.EqualError(t, ccd.Verify(bogusChallenge.String(), ccd.Type, []string{ccd.Origin}, nil, TopOriginIgnoreVerificationMode), "Error validating challenge") } func TestVerifyCollectedClientDataUnexpectedOrigin(t *testing.T) { From cb430d2e299dee4ab08bb9afa64ec6ccd51a3958 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sat, 18 May 2024 23:17:29 +1000 Subject: [PATCH 07/24] temp --- metadata/memory.go | 22 +++++++++--- metadata/types.go | 18 +++++++--- protocol/attestation.go | 58 +++++++++++++++++-------------- protocol/attestation_safetynet.go | 2 +- 4 files changed, 62 insertions(+), 38 deletions(-) diff --git a/metadata/memory.go b/metadata/memory.go index 8411e8c..2673a9f 100644 --- a/metadata/memory.go +++ b/metadata/memory.go @@ -6,12 +6,14 @@ import ( "github.com/google/uuid" ) -// NewMemoryProvider returns a new memory provider given a map, list of undesired AuthenticatorStatus types, and a -// conformance requirement boolean. +// NewMemoryProvider returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a +// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation +// statement, and a validate boolean which determines if trust anchors should be validated by this provider during +// registration. // // If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable // this functionality. -func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired []AuthenticatorStatus, required bool) *MemoryProvider { +func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired []AuthenticatorStatus, required, validate bool) *MemoryProvider { if undesired == nil { undesired = make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) @@ -24,6 +26,7 @@ func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired [] mds: mds, undesired: undesired, require: required, + validate: validate, } } @@ -31,9 +34,18 @@ type MemoryProvider struct { mds map[uuid.UUID]*MetadataBLOBPayloadEntry undesired []AuthenticatorStatus require bool + validate bool } -func (p *MemoryProvider) GetRequireConformance(ctx context.Context) (require bool) { +func (p *MemoryProvider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { + return p.validate +} + +func (p *MemoryProvider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { + return len(p.undesired) > 0 +} + +func (p *MemoryProvider) GetRequireEntry(ctx context.Context) (require bool) { return p.require } @@ -51,7 +63,7 @@ func (p *MemoryProvider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry return nil, nil } -func (p *MemoryProvider) GetIsUndesiredAuthenticatorStatus(ctx context.Context, status AuthenticatorStatus) (isUndesiredAuthenticatorStatus bool) { +func (p *MemoryProvider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status AuthenticatorStatus) (undesired bool) { for _, s := range p.undesired { if s == status { return true diff --git a/metadata/types.go b/metadata/types.go index e0e47c2..fa6ba0d 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -11,14 +11,22 @@ import ( ) type Provider interface { - // GetRequireConformance returns true if this provider requires conformance. - GetRequireConformance(ctx context.Context) (require bool) - // GetEntry returns a MDS3 payload entry given a AAGUID. This GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) - // GetIsUndesiredAuthenticatorStatus returns true if the provided AuthenticatorStatus is not desired. - GetIsUndesiredAuthenticatorStatus(ctx context.Context, status AuthenticatorStatus) (isUndesiredAuthenticatorStatus bool) + // GetRequireEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation + // statement during registration. + GetRequireEntry(ctx context.Context) (require bool) + + // GetTrustAnchorValidation returns true if trust anchor validation of attestation statements is enforced during + // registration. + GetTrustAnchorValidation(ctx context.Context) (validate bool) + + // GetAuthenticatorStatusValidation returns true if this provider should validate the authenticator status reports. + GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) + + // GetAuthenticatorStatusIsUndesired returns true if the provided AuthenticatorStatus is not desired. + GetAuthenticatorStatusIsUndesired(ctx context.Context, status AuthenticatorStatus) (undesired bool) } var ( diff --git a/protocol/attestation.go b/protocol/attestation.go index 9964536..03e293b 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -183,48 +183,52 @@ func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, } if entry == nil { - if mds.GetRequireConformance(ctx) { + if mds.GetRequireEntry(ctx) { return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String())) } return nil } - for _, s := range entry.StatusReports { - if mds.GetIsUndesiredAuthenticatorStatus(ctx, s.Status) { - return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") + if mds.GetAuthenticatorStatusValidation(ctx) { + for _, s := range entry.StatusReports { + if mds.GetAuthenticatorStatusIsUndesired(ctx, s.Status) { + return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") + } } } - if x5cs == nil { - return nil - } - - var ( - x5c *x509.Certificate - raw []byte - ok bool - ) + if mds.GetTrustAnchorValidation(ctx) { + if x5cs == nil { + return nil + } - if len(x5cs) == 0 { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo("The attestation had no certificates") - } + var ( + x5c *x509.Certificate + raw []byte + ok bool + ) - if raw, ok = x5cs[0].([]byte); !ok { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) - } + if len(x5cs) == 0 { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo("The attestation had no certificates") + } - if x5c, err = x509.ParseCertificate(raw); err != nil { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) - } + if raw, ok = x5cs[0].([]byte); !ok { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) + } - if x5c.Subject.CommonName != x5c.Issuer.CommonName { - if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { - return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + if x5c, err = x509.ParseCertificate(raw); err != nil { + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) } - if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) + if x5c.Subject.CommonName != x5c.Issuer.CommonName { + if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { + return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + } + + if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) + } } } diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index cc55a0f..5406c0f 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -138,7 +138,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte, mds met return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time") } else if t.Before(time.Now().Add(-time.Minute)) { // Small tolerance for pre-dated timestamps. - if mds != nil && mds.GetRequireConformance(context.Background()) { + if mds != nil && mds.GetRequireEntry(context.Background()) { return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago") } } From dd241dddc8a2ceb0b5b1f52954ed69e6d0294ceb Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 21 May 2024 23:13:59 +1000 Subject: [PATCH 08/24] feat: allow post creation mds3 validations --- protocol/attestation.go | 10 +++++---- protocol/attestation_test.go | 4 +++- protocol/credential.go | 17 +++++++-------- protocol/credential_test.go | 2 +- webauthn/credential.go | 42 +++++++++++++++++++++++++++++++++--- webauthn/credential_test.go | 2 +- webauthn/login.go | 1 + webauthn/registration.go | 12 ++++++----- webauthn/types.go | 1 + 9 files changed, 67 insertions(+), 24 deletions(-) diff --git a/protocol/attestation.go b/protocol/attestation.go index 03e293b..4153c55 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -67,10 +67,13 @@ type ParsedAttestationResponse struct { type AttestationObject struct { // The authenticator data, including the newly created public key. See AuthenticatorData for more info AuthData AuthenticatorData + // The byteform version of the authenticator data, used in part for signature validation RawAuthData []byte `json:"authData"` + // The format of the Attestation data. Format string `json:"fmt"` + // The attestation statement data sent back if attestation is requested. AttStatement map[string]any `json:"attStmt,omitempty"` } @@ -121,13 +124,12 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon // // Steps 9 through 12 are verified against the auth data. These steps are identical to 11 through 14 for assertion so we // handle them with AuthData. -func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, verificationRequired bool, mds metadata.Provider) error { +func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, mds metadata.Provider) (err error) { rpIDHash := sha256.Sum256([]byte(relyingPartyID)) // Begin Step 9 through 12. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP. - authDataVerificationError := a.AuthData.Verify(rpIDHash[:], nil, verificationRequired) - if authDataVerificationError != nil { - return authDataVerificationError + if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired); err != nil { + return err } // Step 13. Determine the attestation statement format by performing a diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go index 9aa5ae3..86caaf4 100644 --- a/protocol/attestation_test.go +++ b/protocol/attestation_test.go @@ -29,7 +29,9 @@ func TestAttestationVerify(t *testing.T) { pcc.Response = *parsedAttestationResponse - require.NoError(t, pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode, nil)) + _, err = pcc.Verify(options.Response.Challenge.String(), false, options.Response.RelyingParty.ID, []string{options.Response.RelyingParty.Name}, nil, TopOriginIgnoreVerificationMode, nil) + + require.NoError(t, err) }) } } diff --git a/protocol/credential.go b/protocol/credential.go index 64c8928..057d321 100644 --- a/protocol/credential.go +++ b/protocol/credential.go @@ -133,15 +133,15 @@ func (ccr CredentialCreationResponse) Parse() (pcc *ParsedCredentialCreationData // Verify the Client and Attestation data. // // Specification: §7.1. Registering a New Credential (https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential) -func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, mds metadata.Provider) error { +func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, mds metadata.Provider) (clientDataHash []byte, err error) { // Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data - verifyError := pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify) - if verifyError != nil { - return verifyError + if err = pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify); err != nil { + return nil, err } // Step 7. Compute the hash of response.clientDataJSON using SHA-256. - clientDataHash := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) + sum := sha256.Sum256(pcc.Raw.AttestationResponse.ClientDataJSON) + clientDataHash = sum[:] // Step 8. Perform CBOR decoding on the attestationObject field of the AuthenticatorAttestationResponse // structure to obtain the attestation statement format fmt, the authenticator data authData, and the @@ -149,9 +149,8 @@ func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUs // We do the above step while parsing and decoding the CredentialCreationResponse // Handle steps 9 through 14 - This verifies the attestation object. - verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser, mds) - if verifyError != nil { - return verifyError + if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, mds); err != nil { + return clientDataHash, err } // Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root @@ -190,7 +189,7 @@ func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUs // TODO: Not implemented for the reasons mentioned under Step 16 - return nil + return clientDataHash, nil } // GetAppID takes a AuthenticationExtensions object or nil. It then performs the following checks in order: diff --git a/protocol/credential_test.go b/protocol/credential_test.go index dab74c3..b4e7ba8 100644 --- a/protocol/credential_test.go +++ b/protocol/credential_test.go @@ -240,7 +240,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) { Response: tt.fields.Response, Raw: tt.fields.Raw, } - if err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, nil); (err != nil) != tt.wantErr { + if _, err := pcc.Verify(tt.args.storedChallenge.String(), tt.args.verifyUser, tt.args.relyingPartyID, tt.args.relyingPartyOrigin, nil, TopOriginIgnoreVerificationMode, nil); (err != nil) != tt.wantErr { t.Errorf("ParsedCredentialCreationData.Verify() error = %+v, wantErr %v", err, tt.wantErr) } }) diff --git a/webauthn/credential.go b/webauthn/credential.go index 81bcc9a..1cd298a 100644 --- a/webauthn/credential.go +++ b/webauthn/credential.go @@ -1,6 +1,7 @@ package webauthn import ( + "github.com/go-webauthn/webauthn/metadata" "github.com/go-webauthn/webauthn/protocol" ) @@ -26,6 +27,9 @@ type Credential struct { // The Authenticator information for a given certificate. Authenticator Authenticator `json:"authenticator"` + + // The attestation values that can be used to validate this credential via the MDS3 at a later date. + Attestation VerifiableAttestation `json:"attestation"` } type CredentialFlags struct { @@ -54,8 +58,8 @@ func (c Credential) Descriptor() (descriptor protocol.CredentialDescriptor) { } // MakeNewCredential will return a credential pointer on successful validation of a registration response. -func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, error) { - newCredential := &Credential{ +func MakeNewCredential(clientDataHash []byte, s *SessionData, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { + credential = &Credential{ ID: c.Response.AttestationObject.AuthData.AttData.CredentialID, PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey, AttestationType: c.Response.AttestationObject.Format, @@ -71,7 +75,39 @@ func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, e SignCount: c.Response.AttestationObject.AuthData.Counter, Attachment: c.AuthenticatorAttachment, }, + Attestation: VerifiableAttestation{ + RPID: s.RelyingPartyID, + ClientDataHash: clientDataHash, + UserVerificationRequired: s.UserVerification == protocol.VerificationRequired, + RawAuthData: c.Response.AttestationObject.RawAuthData, + AuthData: c.Response.AttestationObject.AuthData, + Format: c.Response.AttestationObject.Format, + AttStatement: c.Response.AttestationObject.AttStatement, + }, + } + + return credential, nil +} + +// VerifiableAttestation is a self-contained attestation from an authenticator that can later be validated against the +// MDS3. +type VerifiableAttestation struct { + RPID string `json:"rpId"` + ClientDataHash []byte `json:"clientDataHash"` + UserVerificationRequired bool `json:"uv"` + RawAuthData []byte `json:"rawAuthData,omitempty"` + AuthData protocol.AuthenticatorData `json:"authData"` + Format string `json:"fmt"` + AttStatement map[string]any `json:"attStmt,omitempty"` +} + +func (a *VerifiableAttestation) Verify(mds metadata.Provider) (err error) { + object := &protocol.AttestationObject{ + AuthData: a.AuthData, + RawAuthData: a.RawAuthData, + Format: a.Format, + AttStatement: a.AttStatement, } - return newCredential, nil + return object.Verify(a.RPID, a.ClientDataHash, a.UserVerificationRequired, mds) } diff --git a/webauthn/credential_test.go b/webauthn/credential_test.go index 8deae1b..9a2ed7e 100644 --- a/webauthn/credential_test.go +++ b/webauthn/credential_test.go @@ -21,7 +21,7 @@ func TestMakeNewCredential(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := MakeNewCredential(tt.args.c) + got, err := MakeNewCredential(nil, nil, tt.args.c) if (err != nil) != tt.wantErr { t.Errorf("MakeNewCredential() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/webauthn/login.go b/webauthn/login.go index 391f35b..bbff80d 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -88,6 +88,7 @@ func (webauthn *WebAuthn) beginLogin(userID []byte, allowedCredentials []protoco session = &SessionData{ Challenge: challenge.String(), + RelyingPartyID: assertion.Response.RelyingPartyID, UserID: userID, AllowedCredentialIDs: assertion.Response.GetAllowedCredentialIDs(), UserVerification: assertion.Response.UserVerification, diff --git a/webauthn/registration.go b/webauthn/registration.go index 6ed8236..a088640 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -91,6 +91,7 @@ func (webauthn *WebAuthn) BeginRegistration(user User, opts ...RegistrationOptio session = &SessionData{ Challenge: challenge.String(), + RelyingPartyID: creation.Response.RelyingParty.ID, UserID: user.WebAuthnID(), UserVerification: creation.Response.AuthenticatorSelection.UserVerification, } @@ -213,7 +214,7 @@ func (webauthn *WebAuthn) FinishRegistration(user User, session SessionData, res } // CreateCredential verifies a parsed response against the user's credentials and session data. -func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parsedResponse *protocol.ParsedCredentialCreationData) (*Credential, error) { +func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parsedResponse *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { if !bytes.Equal(user.WebAuthnID(), session.UserID) { return nil, protocol.ErrBadRequest.WithDetails("ID mismatch for User and Session") } @@ -224,12 +225,13 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse shouldVerifyUser := session.UserVerification == protocol.VerificationRequired - invalidErr := parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.MetaData) - if invalidErr != nil { - return nil, invalidErr + var clientDataHash []byte + + if clientDataHash, err = parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.MetaData); err != nil { + return nil, err } - return MakeNewCredential(parsedResponse) + return MakeNewCredential(clientDataHash, &session, parsedResponse) } func defaultRegistrationCredentialParameters() []protocol.CredentialParameter { diff --git a/webauthn/types.go b/webauthn/types.go index 58415ab..24e3faf 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -203,6 +203,7 @@ type User interface { // ceremony. type SessionData struct { Challenge string `json:"challenge"` + RelyingPartyID string `json:"rpId"` UserID []byte `json:"user_id"` AllowedCredentialIDs [][]byte `json:"allowed_credentials,omitempty"` Expires time.Time `json:"expires"` From 172149a2263996493ff0b19e5c60494ad0b0ada0 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 22 May 2024 23:16:51 +1000 Subject: [PATCH 09/24] feat: post registration verify --- README.md | 46 +++++++++++++++++ protocol/attestation.go | 6 +++ webauthn/credential.go | 98 ++++++++++++++++++++++++------------- webauthn/credential_test.go | 32 ++++++------ webauthn/registration.go | 2 +- 5 files changed, 133 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index a967da0..b4c29db 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,49 @@ func main() { } ``` +## Credential Record + +The WebAuthn Level 3 specification describes the Credential Record which includes several required and optional elements +that you should store for. See [§ 4 Terminology](https://www.w3.org/TR/webauthn-3/#credential-record) for details. + +This section describes this element. + +The fields listed in the specification have corresponding fields in the [webauthn.Credential] struct. See the below +table for more information. We also include JSON mappings for those that wish to just store these values as JSON. + +| Specification Field | Library Field | JSON Field | Notes | +|:-------------------------:|:--------------------------:|:--------------------------:|:-----------------------------------------------------------------------------------------:| +| type | N/A | N/A | This field is always `publicKey` for WebAuthn | +| id | ID | id | | +| publicKey | PublicKey | publicKey | | +| signCount | Authenticator.SignCount | authenticator.signCount | | +| transports | Transport | transport | | +| uvInitialized | Flags.UserVerified | flags.userVerified | | +| backupEligible | Flags.BackupEligible | flags.backupEligible | | +| backupState | Flags.BackupState | flags.backupState | | +| attestationObject | Attestation.Object | attestation.object | This field is a composite of the attestationObject and the relevant values to validate it | +| attestationClientDataJSON | Attestation.ClientDataJSON | attestation.clientDataJSON | | + +### Storage + +It is also important to note that restoring the [webauthn.Credential] with the correct values will likely affect the +validity of the [webauthn.Credential], i.e. if some values are not restored the [webauthn.Credential] may fail +validation in this scenario. + +### Verification + +As long as the [webauthn.Credential] struct has exactly the same values when restored the [Credential Verify] function +can be leveraged to verify the credential against the [metadata.Provider]. This can be either done during registration, +on every login, or with a audit schedule. + +In addition to using the [Credential Verify] function the +[webauthn.Config](https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Config) can contain a provider which will +process all registrations automatically. + +At this time no tooling exists to verify the credential automatically outside the registration flow. Implementation of +this is considered domain logic and beyond the scope of what we provide documentation for; we just provide the necessary +tooling to implement this yourself. + ## Acknowledgements We graciously acknowledge the original authors of this library [github.com/duo-labs/webauthn] for their amazing @@ -297,3 +340,6 @@ implementation. Without their amazing work this library could not exist. [github.com/duo-labs/webauthn]: https://github.com/duo-labs/webauthn +[webauthn.Credential]: https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Credential +[metadata.Provider]: https://pkg.go.dev/github.com/go-webauthn/webauthn/metadata#Provider +[Credential Verify]: https://pkg.go.dev/github.com/go-webauthn/webauthn/webauthn#Credential.Verify \ No newline at end of file diff --git a/protocol/attestation.go b/protocol/attestation.go index 4153c55..5cc045d 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -132,6 +132,12 @@ func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, return err } + return a.VerifyAttestation(clientDataHash, mds) +} + +// VerifyAttestation only verifies the attestation object excluding the AuthData values. If you wish to also verify the +// AuthData values you should use Verify. +func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadata.Provider) (err error) { // Step 13. Determine the attestation statement format by performing a // USASCII case-sensitive match on fmt against the set of supported // WebAuthn Attestation Statement Format Identifier values. The up-to-date diff --git a/webauthn/credential.go b/webauthn/credential.go index 1cd298a..19e45f9 100644 --- a/webauthn/credential.go +++ b/webauthn/credential.go @@ -1,19 +1,22 @@ package webauthn import ( + "crypto/sha256" + "fmt" + "github.com/go-webauthn/webauthn/metadata" "github.com/go-webauthn/webauthn/protocol" ) -// Credential contains all needed information about a WebAuthn credential for storage. +// Credential contains all needed information about a WebAuthn credential for storage. This struct is effectively the +// Credential Record as described in the specification. +// +// See: §4. Terminology: Credential Record (https://www.w3.org/TR/webauthn-3/#credential-record) type Credential struct { - // A probabilistically-unique byte sequence identifying a public key credential source and its authentication assertions. + // The Credential ID of the public key credential source. Described by the Credential Record 'id' field. ID []byte `json:"id"` - // The public key portion of a Relying Party-specific credential key pair, generated by an authenticator and returned to - // a Relying Party at registration time (see also public key credential). The private key portion of the credential key - // pair is known as the credential private key. Note that in the case of self attestation, the credential key pair is also - // used as the attestation key pair, see self attestation for details. + // The credential public key of the public key credential source. Described by the Credential Record 'publicKey field. PublicKey []byte `json:"publicKey"` // The attestation format used (if any) by the authenticator when creating the credential. @@ -29,7 +32,7 @@ type Credential struct { Authenticator Authenticator `json:"authenticator"` // The attestation values that can be used to validate this credential via the MDS3 at a later date. - Attestation VerifiableAttestation `json:"attestation"` + Attestation CredentialAttestation `json:"attestation"` } type CredentialFlags struct { @@ -47,6 +50,14 @@ type CredentialFlags struct { BackupState bool `json:"backupState"` } +type CredentialAttestation struct { + ClientDataJSON []byte `json:"clientDataJSON"` + ClientDataHash []byte `json:"clientDataHash"` + AuthenticatorData []byte `json:"authenticatorData"` + PublicKeyAlgorithm int64 `json:"publicKeyAlgorithm"` + Object []byte `json:"object"` +} + // Descriptor converts a Credential into a protocol.CredentialDescriptor. func (c Credential) Descriptor() (descriptor protocol.CredentialDescriptor) { return protocol.CredentialDescriptor{ @@ -57,8 +68,8 @@ func (c Credential) Descriptor() (descriptor protocol.CredentialDescriptor) { } } -// MakeNewCredential will return a credential pointer on successful validation of a registration response. -func MakeNewCredential(clientDataHash []byte, s *SessionData, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { +// NewCredential will return a credential pointer on successful validation of a registration response. +func NewCredential(clientDataHash []byte, c *protocol.ParsedCredentialCreationData) (credential *Credential, err error) { credential = &Credential{ ID: c.Response.AttestationObject.AuthData.AttData.CredentialID, PublicKey: c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey, @@ -75,39 +86,56 @@ func MakeNewCredential(clientDataHash []byte, s *SessionData, c *protocol.Parsed SignCount: c.Response.AttestationObject.AuthData.Counter, Attachment: c.AuthenticatorAttachment, }, - Attestation: VerifiableAttestation{ - RPID: s.RelyingPartyID, - ClientDataHash: clientDataHash, - UserVerificationRequired: s.UserVerification == protocol.VerificationRequired, - RawAuthData: c.Response.AttestationObject.RawAuthData, - AuthData: c.Response.AttestationObject.AuthData, - Format: c.Response.AttestationObject.Format, - AttStatement: c.Response.AttestationObject.AttStatement, + Attestation: CredentialAttestation{ + ClientDataJSON: c.Raw.AttestationResponse.ClientDataJSON, + ClientDataHash: clientDataHash, + AuthenticatorData: c.Raw.AttestationResponse.AuthenticatorData, + PublicKeyAlgorithm: c.Raw.AttestationResponse.PublicKeyAlgorithm, + Object: c.Raw.AttestationResponse.AttestationObject, }, } return credential, nil } -// VerifiableAttestation is a self-contained attestation from an authenticator that can later be validated against the -// MDS3. -type VerifiableAttestation struct { - RPID string `json:"rpId"` - ClientDataHash []byte `json:"clientDataHash"` - UserVerificationRequired bool `json:"uv"` - RawAuthData []byte `json:"rawAuthData,omitempty"` - AuthData protocol.AuthenticatorData `json:"authData"` - Format string `json:"fmt"` - AttStatement map[string]any `json:"attStmt,omitempty"` -} +// Verify this credentials against the metadata.Provider given. +func (c Credential) Verify(mds metadata.Provider) (err error) { + if mds == nil { + return fmt.Errorf("error verifying credential: the metadata provider must be provided but it's nil") + } + + raw := &protocol.AuthenticatorAttestationResponse{ + AuthenticatorResponse: protocol.AuthenticatorResponse{ + ClientDataJSON: c.Attestation.ClientDataJSON, + }, + Transports: make([]string, len(c.Transport)), + AuthenticatorData: c.Attestation.AuthenticatorData, + PublicKey: c.PublicKey, + PublicKeyAlgorithm: c.Attestation.PublicKeyAlgorithm, + AttestationObject: c.Attestation.Object, + } + + for i, transport := range c.Transport { + raw.Transports[i] = string(transport) + } + + var attestation *protocol.ParsedAttestationResponse + + if attestation, err = raw.Parse(); err != nil { + return fmt.Errorf("error verifying credential: error parsing attestation: %w", err) + } + + clientDataHash := c.Attestation.ClientDataHash + + if len(clientDataHash) == 0 { + sum := sha256.Sum256(c.Attestation.ClientDataJSON) + + clientDataHash = sum[:] + } -func (a *VerifiableAttestation) Verify(mds metadata.Provider) (err error) { - object := &protocol.AttestationObject{ - AuthData: a.AuthData, - RawAuthData: a.RawAuthData, - Format: a.Format, - AttStatement: a.AttStatement, + if err = attestation.AttestationObject.VerifyAttestation(clientDataHash, mds); err != nil { + return fmt.Errorf("error verifying credential: error verifying attestation: %w", err) } - return object.Verify(a.RPID, a.ClientDataHash, a.UserVerificationRequired, mds) + return nil } diff --git a/webauthn/credential_test.go b/webauthn/credential_test.go index 9a2ed7e..d70b705 100644 --- a/webauthn/credential_test.go +++ b/webauthn/credential_test.go @@ -1,9 +1,11 @@ package webauthn import ( - "reflect" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/go-webauthn/webauthn/protocol" ) @@ -12,22 +14,22 @@ func TestMakeNewCredential(t *testing.T) { c *protocol.ParsedCredentialCreationData } - var tests []struct { - name string - args args - want *Credential - wantErr bool + var testCases []struct { + name string + args args + expected *Credential + err string } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := MakeNewCredential(nil, nil, tt.args.c) - if (err != nil) != tt.wantErr { - t.Errorf("MakeNewCredential() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("MakeNewCredential() = %v, want %v", got, tt.want) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual, err := NewCredential(nil, tc.args.c) + if len(tc.err) > 0 { + assert.EqualError(t, err, tc.err) + } else { + require.NoError(t, err) + + assert.EqualValues(t, tc.expected, actual) } }) } diff --git a/webauthn/registration.go b/webauthn/registration.go index a088640..3352eb2 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -231,7 +231,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse return nil, err } - return MakeNewCredential(clientDataHash, &session, parsedResponse) + return NewCredential(clientDataHash, parsedResponse) } func defaultRegistrationCredentialParameters() []protocol.CredentialParameter { From 3d21aa1fe1561c95d69e637eca2f0a5516389d7b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 3 Jun 2024 06:53:02 +1000 Subject: [PATCH 10/24] feat: desired status --- metadata/providers/memory/const.go | 12 ++++ metadata/providers/memory/opts.go | 15 +++++ metadata/providers/memory/provider.go | 86 +++++++++++++++++++++++++++ metadata/status.go | 62 +++++++++++++++++++ metadata/types.go | 7 +-- protocol/attestation.go | 8 +-- 6 files changed, 179 insertions(+), 11 deletions(-) create mode 100644 metadata/providers/memory/const.go create mode 100644 metadata/providers/memory/opts.go create mode 100644 metadata/providers/memory/provider.go create mode 100644 metadata/status.go diff --git a/metadata/providers/memory/const.go b/metadata/providers/memory/const.go new file mode 100644 index 0000000..d86630e --- /dev/null +++ b/metadata/providers/memory/const.go @@ -0,0 +1,12 @@ +package memory + +import "github.com/go-webauthn/webauthn/metadata" + +// defaultUndesiredAuthenticatorStatus is an array of undesirable authenticator statuses +var defaultUndesiredAuthenticatorStatus = [...]metadata.AuthenticatorStatus{ + metadata.AttestationKeyCompromise, + metadata.UserVerificationBypass, + metadata.UserKeyRemoteCompromise, + metadata.UserKeyPhysicalCompromise, + metadata.Revoked, +} diff --git a/metadata/providers/memory/opts.go b/metadata/providers/memory/opts.go new file mode 100644 index 0000000..509e381 --- /dev/null +++ b/metadata/providers/memory/opts.go @@ -0,0 +1,15 @@ +package memory + +import ( + "github.com/google/uuid" + + "github.com/go-webauthn/webauthn/metadata" +) + +type Opt func(*Provider) + +func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Opt { + return func(provider *Provider) { + provider.mds = metadata + } +} diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go new file mode 100644 index 0000000..7cb8caa --- /dev/null +++ b/metadata/providers/memory/provider.go @@ -0,0 +1,86 @@ +package memory + +import ( + "context" + + "github.com/google/uuid" + + "github.com/go-webauthn/webauthn/metadata" +) + +// New returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a +// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation +// statement, and a validate boolean which determines if trust anchors should be validated by this provider during +// registration. +// +// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable +// this functionality. +func New(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry, undesired []metadata.AuthenticatorStatus, required, validate bool) *Provider { + if undesired == nil { + undesired = make([]metadata.AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) + + for i := range defaultUndesiredAuthenticatorStatus { + undesired[i] = defaultUndesiredAuthenticatorStatus[i] + } + } + + return &Provider{ + mds: mds, + undesired: undesired, + require: required, + validate: validate, + } +} + +type Provider struct { + mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry + desired []metadata.AuthenticatorStatus + undesired []metadata.AuthenticatorStatus + require bool + validate bool + status bool +} + +func (p *Provider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { + return p.validate +} + +func (p *Provider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { + return len(p.undesired) > 0 +} + +func (p *Provider) GetRequireEntry(ctx context.Context) (require bool) { + return p.require +} + +func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.MetadataBLOBPayloadEntry, err error) { + if p.mds == nil { + return nil, metadata.ErrNotInitialized + } + + var ok bool + + if entry, ok = p.mds[aaguid]; ok { + return entry, nil + } + + return nil, nil +} + +func (p *Provider) ValidateAuthenticatorStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { + if !p.status { + return nil + } + + return metadata.ValidateStatusReports(reports, p.desired, p.undesired) +} + +func (p *Provider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status metadata.AuthenticatorStatus) (undesired bool) { + for _, s := range p.undesired { + if s == status { + return true + } + } + + return false +} diff --git a/metadata/status.go b/metadata/status.go new file mode 100644 index 0000000..158e37c --- /dev/null +++ b/metadata/status.go @@ -0,0 +1,62 @@ +package metadata + +import ( + "fmt" + "strings" +) + +// ValidateStatusReports checks a list of StatusReport's against a list of desired and undesired AuthenticatorStatus +// values. If the reports contain all of the desired and none of the undesired status reports then no error is returned +// otherwise an error describing the issue is returned. +func ValidateStatusReports(reports []StatusReport, desired, undesired []AuthenticatorStatus) (err error) { + if len(desired) == 0 && (len(undesired) == 0 || len(reports) == 0) { + return nil + } + + var present, absent []string + + if len(undesired) != 0 { + for _, report := range reports { + for _, status := range undesired { + if report.Status == status { + present = append(present, string(status)) + + continue + } + } + } + } + + if len(desired) != 0 { + desired: + for _, status := range desired { + for _, report := range reports { + if report.Status == status { + continue desired + } + } + + absent = append(absent, string(status)) + } + } + + switch { + case len(present) == 0 && len(absent) == 0: + return nil + case len(present) != 0 && len(absent) == 0: + return &MetadataError{ + Type: "invalid_status", + Details: fmt.Sprintf("The following undesired status reports were present: %s", strings.Join(present, ", ")), + } + case len(present) == 0 && len(absent) != 0: + return &MetadataError{ + Type: "invalid_status", + Details: fmt.Sprintf("The following desired status reports were absent: %s", strings.Join(absent, ", ")), + } + default: + return &MetadataError{ + Type: "invalid_status", + Details: fmt.Sprintf("The following undesired status reports were present: %s; the following desired status reports were absent: %s", strings.Join(present, ", "), strings.Join(absent, ", ")), + } + } +} diff --git a/metadata/types.go b/metadata/types.go index fa6ba0d..71d204c 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -22,11 +22,8 @@ type Provider interface { // registration. GetTrustAnchorValidation(ctx context.Context) (validate bool) - // GetAuthenticatorStatusValidation returns true if this provider should validate the authenticator status reports. - GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) - - // GetAuthenticatorStatusIsUndesired returns true if the provided AuthenticatorStatus is not desired. - GetAuthenticatorStatusIsUndesired(ctx context.Context, status AuthenticatorStatus) (undesired bool) + // ValidateAuthenticatorStatusReports returns nil if the provided authenticator status reports are desired. + ValidateAuthenticatorStatusReports(ctx context.Context, reports []StatusReport) (err error) } var ( diff --git a/protocol/attestation.go b/protocol/attestation.go index 5cc045d..9f14fbb 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -198,12 +198,8 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat return nil } - if mds.GetAuthenticatorStatusValidation(ctx) { - for _, s := range entry.StatusReports { - if mds.GetAuthenticatorStatusIsUndesired(ctx, s.Status) { - return ErrInvalidAttestation.WithDetails("Authenticator with undesirable status encountered") - } - } + if err = mds.ValidateAuthenticatorStatusReports(ctx, entry.StatusReports); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered. %s", err.Error())) } if mds.GetTrustAnchorValidation(ctx) { From 4c93c70c8363267902298a2c1175a1536f2a2998 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 3 Jun 2024 22:49:45 +1000 Subject: [PATCH 11/24] feat: refactoring --- metadata/memory.go | 74 ----------------------- metadata/metadata.go | 13 ++++- metadata/passkey_authenticator.go | 11 +++- metadata/providers/memory/const.go | 12 ---- metadata/providers/memory/opts.go | 55 +++++++++++++++++- metadata/providers/memory/provider.go | 84 ++++++++++++--------------- metadata/types.go | 21 +++++-- protocol/attestation.go | 34 ++++++----- protocol/attestation_safetynet.go | 2 +- 9 files changed, 147 insertions(+), 159 deletions(-) delete mode 100644 metadata/memory.go delete mode 100644 metadata/providers/memory/const.go diff --git a/metadata/memory.go b/metadata/memory.go deleted file mode 100644 index 2673a9f..0000000 --- a/metadata/memory.go +++ /dev/null @@ -1,74 +0,0 @@ -package metadata - -import ( - "context" - - "github.com/google/uuid" -) - -// NewMemoryProvider returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a -// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation -// statement, and a validate boolean which determines if trust anchors should be validated by this provider during -// registration. -// -// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable -// this functionality. -func NewMemoryProvider(mds map[uuid.UUID]*MetadataBLOBPayloadEntry, undesired []AuthenticatorStatus, required, validate bool) *MemoryProvider { - if undesired == nil { - undesired = make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) - - for i := range defaultUndesiredAuthenticatorStatus { - undesired[i] = defaultUndesiredAuthenticatorStatus[i] - } - } - - return &MemoryProvider{ - mds: mds, - undesired: undesired, - require: required, - validate: validate, - } -} - -type MemoryProvider struct { - mds map[uuid.UUID]*MetadataBLOBPayloadEntry - undesired []AuthenticatorStatus - require bool - validate bool -} - -func (p *MemoryProvider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { - return p.validate -} - -func (p *MemoryProvider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { - return len(p.undesired) > 0 -} - -func (p *MemoryProvider) GetRequireEntry(ctx context.Context) (require bool) { - return p.require -} - -func (p *MemoryProvider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) { - if p.mds == nil { - return nil, ErrNotInitialized - } - - var ok bool - - if entry, ok = p.mds[aaguid]; ok { - return entry, nil - } - - return nil, nil -} - -func (p *MemoryProvider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status AuthenticatorStatus) (undesired bool) { - for _, s := range p.undesired { - if s == status { - return true - } - } - - return false -} diff --git a/metadata/metadata.go b/metadata/metadata.go index 9cbc9f6..2907e8a 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -19,7 +19,7 @@ func (m *Metadata) ToMap() (metadata map[uuid.UUID]*MetadataBLOBPayloadEntry) { metadata = make(map[uuid.UUID]*MetadataBLOBPayloadEntry) for _, entry := range m.Parsed.Entries { - if entry.AaGUID.ID() != 0 { + if entry.AaGUID != uuid.Nil { metadata[entry.AaGUID] = &entry } } @@ -875,3 +875,14 @@ type MDSGetEndpointsResponse struct { // An array of urls, each pointing to a MetadataTOCPayload. Result []string `json:"result"` } + +// DefaultUndesiredAuthenticatorStatuses returns a copy of the defaultUndesiredAuthenticatorStatus slice. +func DefaultUndesiredAuthenticatorStatuses() []AuthenticatorStatus { + undesired := make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) + + for i := range defaultUndesiredAuthenticatorStatus { + undesired[i] = defaultUndesiredAuthenticatorStatus[i] + } + + return undesired +} diff --git a/metadata/passkey_authenticator.go b/metadata/passkey_authenticator.go index b057600..bd993d0 100644 --- a/metadata/passkey_authenticator.go +++ b/metadata/passkey_authenticator.go @@ -1,9 +1,16 @@ package metadata +// PasskeyAuthenticator is a type that represents the schema from the Passkey Developer AAGUID listing. +// +// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids type PasskeyAuthenticator map[string]PassKeyAuthenticatorAAGUID +// PassKeyAuthenticatorAAGUID is a type that represents the indivudal schema entry from the Passkey Developer AAGUID +// listing. Used with PasskeyAuthenticator. +// +// See: https://github.com/passkeydeveloper/passkey-authenticator-aaguids type PassKeyAuthenticatorAAGUID struct { Name string `json:"name"` - IconDark string `json:"icon_dark"` - IconLight string `json:"icon_light"` + IconDark string `json:"icon_dark,omitempty"` + IconLight string `json:"icon_light,omitempty"` } diff --git a/metadata/providers/memory/const.go b/metadata/providers/memory/const.go deleted file mode 100644 index d86630e..0000000 --- a/metadata/providers/memory/const.go +++ /dev/null @@ -1,12 +0,0 @@ -package memory - -import "github.com/go-webauthn/webauthn/metadata" - -// defaultUndesiredAuthenticatorStatus is an array of undesirable authenticator statuses -var defaultUndesiredAuthenticatorStatus = [...]metadata.AuthenticatorStatus{ - metadata.AttestationKeyCompromise, - metadata.UserVerificationBypass, - metadata.UserKeyRemoteCompromise, - metadata.UserKeyPhysicalCompromise, - metadata.Revoked, -} diff --git a/metadata/providers/memory/opts.go b/metadata/providers/memory/opts.go index 509e381..4d9f8fc 100644 --- a/metadata/providers/memory/opts.go +++ b/metadata/providers/memory/opts.go @@ -6,10 +6,61 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -type Opt func(*Provider) +// Option describes an optional pattern for this provider. +type Option func(*Provider) -func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Opt { +// WithMetadata provides the required metadata for the memory provider. +func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option { return func(provider *Provider) { provider.mds = metadata } } + +// WithValidateEntry requires that the provided metadata has an entry for the given authenticator to be considered +// valid. By default an AAGUID which has a zero value should fail validation if WithValidateEntryPermitZeroAAGUID is not +// provided with the value of true. +func WithValidateEntry(require bool) Option { + return func(provider *Provider) { + provider.entry = require + } +} + +// WithValidateEntryPermitZeroAAGUID is an option that permits a zero'd AAGUID from an attestation statement to +// automatically pass metadata validations. Generally helpful to use with WithValidateEntry. +func WithValidateEntryPermitZeroAAGUID(permit bool) Option { + return func(provider *Provider) { + provider.entryPermitZero = permit + } +} + +// WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor +// from the metadata. +func WithValidateTrustAnchor(validate bool) Option { + return func(provider *Provider) { + provider.anchors = validate + } +} + +// WithValidateStatus when set to true enables the validation of the attestation statments AAGUID against the desired +// and undesired metadata.AuthenticatorStatus lists. +func WithValidateStatus(validate bool) Option { + return func(provider *Provider) { + provider.status = validate + } +} + +// WithStatusUndesired provides the list of statuses which are considered undesirable for status report validation +// purposes. Should be used with WithValidateStatus set to true. +func WithStatusUndesired(statuses []metadata.AuthenticatorStatus) Option { + return func(provider *Provider) { + provider.undesired = statuses + } +} + +// WithStatusDesired provides the list of statuses which are considered desired and will be required for status report +// validation purposes. Should be used with WithValidateStatus set to true. +func WithStatusDesired(statuses []metadata.AuthenticatorStatus) Option { + return func(provider *Provider) { + provider.desired = statuses + } +} diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index 7cb8caa..775e600 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -8,49 +8,31 @@ import ( "github.com/go-webauthn/webauthn/metadata" ) -// New returns a new memory provider given a map, list of undesired AuthenticatorStatus types, a -// required boolean which if true will cause registrations to fail if no metadata entry is found for the attestation -// statement, and a validate boolean which determines if trust anchors should be validated by this provider during -// registration. -// -// If the undesired status slice is nil it will use a default value. You must explicitly use an empty slice to disable -// this functionality. -func New(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry, undesired []metadata.AuthenticatorStatus, required, validate bool) *Provider { - if undesired == nil { - undesired = make([]metadata.AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) - - for i := range defaultUndesiredAuthenticatorStatus { - undesired[i] = defaultUndesiredAuthenticatorStatus[i] - } - } +// New returns a new memory provider given a set of functional Option's. +func New(opts ...Option) (provider *Provider) { - return &Provider{ - mds: mds, - undesired: undesired, - require: required, - validate: validate, + provider = &Provider{ + undesired: metadata.DefaultUndesiredAuthenticatorStatuses(), } -} - -type Provider struct { - mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry - desired []metadata.AuthenticatorStatus - undesired []metadata.AuthenticatorStatus - require bool - validate bool - status bool -} -func (p *Provider) GetTrustAnchorValidation(ctx context.Context) (validate bool) { - return p.validate -} + for _, opt := range opts { + opt(provider) + } -func (p *Provider) GetAuthenticatorStatusValidation(ctx context.Context) (validate bool) { - return len(p.undesired) > 0 + return provider } -func (p *Provider) GetRequireEntry(ctx context.Context) (require bool) { - return p.require +// Provider is a concrete implementation of the metadata.Provider that utilizes memory for validation. This provider is +// a simple one-shot that doesn't perform any locking, provide dynamic functionality, or download the metadata at any +// stage (it expects it's provided via one of the Option's). +type Provider struct { + mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry + desired []metadata.AuthenticatorStatus + undesired []metadata.AuthenticatorStatus + entry bool + entryPermitZero bool + anchors bool + status bool } func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.MetadataBLOBPayloadEntry, err error) { @@ -67,20 +49,26 @@ func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metad return nil, nil } -func (p *Provider) ValidateAuthenticatorStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { - if !p.status { - return nil - } +func (p *Provider) GetValidateEntry(ctx context.Context) (require bool) { + return p.entry +} - return metadata.ValidateStatusReports(reports, p.desired, p.undesired) +func (p *Provider) GetValidateEntryPermitZeroAAGUID(ctx context.Context) (skip bool) { + return p.entryPermitZero +} + +func (p *Provider) GetValidateTrustAnchor(ctx context.Context) (validate bool) { + return p.anchors } -func (p *Provider) GetAuthenticatorStatusIsUndesired(ctx context.Context, status metadata.AuthenticatorStatus) (undesired bool) { - for _, s := range p.undesired { - if s == status { - return true - } +func (p *Provider) GetValidateStatus(ctx context.Context) (validate bool) { + return p.status +} + +func (p *Provider) ValidateStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { + if !p.status { + return nil } - return false + return metadata.ValidateStatusReports(reports, p.desired, p.undesired) } diff --git a/metadata/types.go b/metadata/types.go index 71d204c..fbcf9eb 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -14,16 +14,25 @@ type Provider interface { // GetEntry returns a MDS3 payload entry given a AAGUID. This GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) - // GetRequireEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation + // GetValidateEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation // statement during registration. - GetRequireEntry(ctx context.Context) (require bool) + GetValidateEntry(ctx context.Context) (validate bool) - // GetTrustAnchorValidation returns true if trust anchor validation of attestation statements is enforced during + // GetValidateEntryPermitZeroAAGUID returns true if attestation statements with zerod AAGUID should be permitted + // when considering the result from GetValidateEntry. i.e. if the AAGUID is zeroed, and GetValidateEntry returns + // true, and this implementation returns true, the attestation statement will pass validation. + GetValidateEntryPermitZeroAAGUID(ctx context.Context) (skip bool) + + // GetValidateTrustAnchor returns true if trust anchor validation of attestation statements is enforced during // registration. - GetTrustAnchorValidation(ctx context.Context) (validate bool) + GetValidateTrustAnchor(ctx context.Context) (validate bool) + + // GetValidateStatus returns true if the status reports for an authenticator should be validated against desired and + // undesired statuses. + GetValidateStatus(ctx context.Context) (validate bool) - // ValidateAuthenticatorStatusReports returns nil if the provided authenticator status reports are desired. - ValidateAuthenticatorStatusReports(ctx context.Context, reports []StatusReport) (err error) + // ValidateStatusReports returns nil if the provided authenticator status reports are desired. + ValidateStatusReports(ctx context.Context, reports []StatusReport) (err error) } var ( diff --git a/protocol/attestation.go b/protocol/attestation.go index 9f14fbb..90979e3 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -176,8 +176,10 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat entry *metadata.MetadataBLOBPayloadEntry ) - if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { - return err + if len(a.AuthData.AttData.AAGUID) != 0 { + if aaguid, err = uuid.FromBytes(a.AuthData.AttData.AAGUID); err != nil { + return ErrInvalidAttestation.WithInfo("Error occurred parsing AAGUID during attestation validation").WithDetails(err.Error()) + } } if mds == nil { @@ -187,22 +189,28 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat ctx := context.Background() if entry, err = mds.GetEntry(ctx, aaguid); err != nil { - return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred: %+v", err)).WithDetails(fmt.Sprintf("Error occurred looking up entry for AAGUID %s", aaguid.String())) + return ErrInvalidAttestation.WithInfo(fmt.Sprintf("Error occurred retrieving metadata entry during attestation validation: %+v", err)).WithDetails(fmt.Sprintf("Error occurred looking up entry for AAGUID %s", aaguid.String())) } if entry == nil { - if mds.GetRequireEntry(ctx) { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", aaguid.String())) + if aaguid == uuid.Nil && mds.GetValidateEntryPermitZeroAAGUID(ctx) { + return nil + } + + if mds.GetValidateEntry(ctx) { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during attestation validation", aaguid.String())) } return nil } - if err = mds.ValidateAuthenticatorStatusReports(ctx, entry.StatusReports); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered. %s", err.Error())) + if mds.GetValidateStatus(ctx) { + if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered during attestation validation. %s", err.Error())) + } } - if mds.GetTrustAnchorValidation(ctx) { + if mds.GetValidateTrustAnchor(ctx) { if x5cs == nil { return nil } @@ -214,24 +222,24 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat ) if len(x5cs) == 0 { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo("The attestation had no certificates") + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo("The attestation had no certificates") } if raw, ok = x5cs[0].([]byte); !ok { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo(fmt.Sprintf("The first certificate in the attestation was type '%T' but '[]byte' was expected", x5cs[0])) } if x5c, err = x509.ParseCertificate(raw); err != nil { - return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) + return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c during attestation validation").WithInfo(fmt.Sprintf("Error returned from x509.ParseCertificate: %+v", err)) } if x5c.Subject.CommonName != x5c.Issuer.CommonName { if !entry.MetadataStatement.AttestationTypes.HasBasicFull() { - return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authenticator that does not support full attestation") + return ErrInvalidAttestation.WithDetails("Unable to validate attestation statement signature during attestation validation: attestation with full attestation from authenticator that does not support full attestation") } if _, err = x5c.Verify(entry.MetadataStatement.Verifier()); err != nil { - return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Invalid certificate chain from MDS: %v", err)) + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Unable to validate attestation signature statement during attestation validation: invalid certificate chain from MDS: %v", err)) } } } diff --git a/protocol/attestation_safetynet.go b/protocol/attestation_safetynet.go index 5406c0f..849cda1 100644 --- a/protocol/attestation_safetynet.go +++ b/protocol/attestation_safetynet.go @@ -138,7 +138,7 @@ func verifySafetyNetFormat(att AttestationObject, clientDataHash []byte, mds met return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp after current time") } else if t.Before(time.Now().Add(-time.Minute)) { // Small tolerance for pre-dated timestamps. - if mds != nil && mds.GetRequireEntry(context.Background()) { + if mds != nil && mds.GetValidateEntry(context.Background()) { return "", nil, ErrInvalidAttestation.WithDetails("SafetyNet response with timestamp before one minute ago") } } From 9b87b9f23b4d85d3bc421997d28a19f54732d2cb Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 9 Jun 2024 11:02:53 +1000 Subject: [PATCH 12/24] feat: add tooling --- metadata/decode.go | 2 ++ metadata/metadata.go | 28 +++++++++++++++++++++++++++ metadata/providers/memory/provider.go | 4 ++++ 3 files changed, 34 insertions(+) diff --git a/metadata/decode.go b/metadata/decode.go index 293ea9f..b83eecc 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -248,6 +248,7 @@ func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err e return certificate, nil } +// DecoderOption is a representation of a function that can set options within a decoder. type DecoderOption func(decoder *Decoder) (err error) // WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for @@ -260,6 +261,7 @@ func WithIgnoreEntryParsingErrors() DecoderOption { } } +// WithRootCertificate overrides the root certificate used to validate the authenticity of the metadata payload. func WithRootCertificate(value string) DecoderOption { return func(decoder *Decoder) (err error) { decoder.root = value diff --git a/metadata/metadata.go b/metadata/metadata.go index 2907e8a..64fb9eb 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -3,6 +3,7 @@ package metadata import ( "crypto/x509" "fmt" + "net/http" "net/url" "strings" "time" @@ -10,6 +11,33 @@ import ( "github.com/google/uuid" ) +// Fetch creates a new HTTP client and gets the production metadata, decodes it, and parses it. This is an +// instrumentation simplification that makes it easier to either just grab the latest metadata or for implementers to +// see the rough process of retrieving it to implement any of their own logic. +func Fetch() (metadata *Metadata, err error) { + var ( + decoder *Decoder + payload *MetadataBLOBPayloadJSON + res *http.Response + ) + + if decoder, err = NewDecoder(); err != nil { + return nil, err + } + + client := &http.Client{} + + if res, err = client.Get(ProductionMDSURL); err != nil { + return nil, err + } + + if payload, err = decoder.Decode(res.Body); err != nil { + return nil, err + } + + return decoder.Parse(payload) +} + type Metadata struct { Parsed MetadataBLOBPayload Unparsed []MetadataBLOBPayloadEntryError diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index 775e600..e60a080 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -72,3 +72,7 @@ func (p *Provider) ValidateStatusReports(ctx context.Context, reports []metadata return metadata.ValidateStatusReports(reports, p.desired, p.undesired) } + +var ( + _ metadata.Provider = (*Provider)(nil) +) From 04dc15eb743a20b0985fbc6b3d90de1cad3aa89e Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 9 Jun 2024 11:59:57 +1000 Subject: [PATCH 13/24] refactor: misc --- metadata/decode.go | 50 +++++++++-------- metadata/metadata.go | 2 +- metadata/metadata_test.go | 17 ------ metadata/types.go | 109 ++++++++++++++++++++++++++++++-------- 4 files changed, 114 insertions(+), 64 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index b83eecc..db03a09 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -36,6 +36,7 @@ func NewDecoder(opts ...DecoderOption) (decoder *Decoder, err error) { return decoder, nil } +// Decoder handles decoding and specialized parsing of the metadata blob. type Decoder struct { client *http.Client parser *jwt.Parser @@ -44,6 +45,7 @@ type Decoder struct { ignoreEntryParsingErrors bool } +// Parse handles parsing of the raw JSON values of the metadata blob. Should be used after using Decode or DecodeBytes. func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, err error) { metadata = &Metadata{ Parsed: MetadataBLOBPayload{ @@ -53,7 +55,7 @@ func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, e } if metadata.Parsed.NextUpdate, err = time.Parse(time.DateOnly, payload.NextUpdate); err != nil { - return nil, fmt.Errorf("error occurred parsing next update value: %w", err) + return nil, fmt.Errorf("error occurred parsing next update value '%s': %w", payload.NextUpdate, err) } var parsed MetadataBLOBPayloadEntry @@ -78,6 +80,7 @@ func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, e return metadata, nil } +// Decode the blob from an io.ReadCloser. This function will close the io.ReadCloser after completing. func (d *Decoder) Decode(r io.ReadCloser) (payload *MetadataBLOBPayloadJSON, err error) { defer r.Close() @@ -89,6 +92,7 @@ func (d *Decoder) Decode(r io.ReadCloser) (payload *MetadataBLOBPayloadJSON, err return d.DecodeBytes(bytes) } +// DecodeBytes handles decoding raw bytes. If you have a read closer it's suggested to use Decode. func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, err error) { payload = &MetadataBLOBPayloadJSON{} @@ -162,6 +166,28 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, e return payload, nil } +// DecoderOption is a representation of a function that can set options within a decoder. +type DecoderOption func(decoder *Decoder) (err error) + +// WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for +// these entries will exist as an unparsed entry. +func WithIgnoreEntryParsingErrors() DecoderOption { + return func(decoder *Decoder) (err error) { + decoder.ignoreEntryParsingErrors = true + + return nil + } +} + +// WithRootCertificate overrides the root certificate used to validate the authenticity of the metadata payload. +func WithRootCertificate(value string) DecoderOption { + return func(decoder *Decoder) (err error) { + decoder.root = value + + return nil + } +} + func validateChain(root string, chain []any) (bool, error) { oRoot := make([]byte, base64.StdEncoding.DecodedLen(len(root))) @@ -247,25 +273,3 @@ func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err e return certificate, nil } - -// DecoderOption is a representation of a function that can set options within a decoder. -type DecoderOption func(decoder *Decoder) (err error) - -// WithIgnoreEntryParsingErrors is a DecoderOption which ignores errors when parsing individual entries. The values for -// these entries will exist as an unparsed entry. -func WithIgnoreEntryParsingErrors() DecoderOption { - return func(decoder *Decoder) (err error) { - decoder.ignoreEntryParsingErrors = true - - return nil - } -} - -// WithRootCertificate overrides the root certificate used to validate the authenticity of the metadata payload. -func WithRootCertificate(value string) DecoderOption { - return func(decoder *Decoder) (err error) { - decoder.root = value - - return nil - } -} diff --git a/metadata/metadata.go b/metadata/metadata.go index 64fb9eb..ce81885 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -90,7 +90,7 @@ func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error var update time.Time if update, err = time.Parse(time.DateOnly, j.NextUpdate); err != nil { - return payload, fmt.Errorf("error occurred parsing next update value: %w", err) + return payload, fmt.Errorf("error occurred parsing next update value '%s': %w", j.NextUpdate, err) } n := len(j.Entries) diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index e55878e..cadecb3 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -294,23 +294,6 @@ func TestAlgKeyMatch(t *testing.T) { } } -func download(url string, c *http.Client) { - -} - -func downloadBytes(url string, c *http.Client) ([]byte, error) { - res, err := c.Get(url) - if err != nil { - return nil, err - } - - defer res.Body.Close() - - body, _ := io.ReadAll(res.Body) - - return body, err -} - func getEndpoints(c *http.Client) ([]string, error) { jsonReq, err := json.Marshal(MDSGetEndpointsRequest{Endpoint: "https://webauthn.io"}) if err != nil { diff --git a/metadata/types.go b/metadata/types.go index fbcf9eb..84474b0 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -63,14 +63,19 @@ type AuthenticatorAttestationType string const ( // BasicFull - Indicates full basic attestation, based on an attestation private key shared among a class of authenticators (e.g. same model). Authenticators must provide its attestation signature during the registration process for the same reason. The attestation trust anchor is shared with FIDO Servers out of band (as part of the Metadata). This sharing process should be done according to [UAFMetadataService]. BasicFull AuthenticatorAttestationType = "basic_full" + // BasicSurrogate - Just syntactically a Basic Attestation. The attestation object self-signed, i.e. it is signed using the UAuth.priv key, i.e. the key corresponding to the UAuth.pub key included in the attestation object. As a consequence it does not provide a cryptographic proof of the security characteristics. But it is the best thing we can do if the authenticator is not able to have an attestation private key. BasicSurrogate AuthenticatorAttestationType = "basic_surrogate" + // Ecdaa - Indicates use of elliptic curve based direct anonymous attestation as defined in [FIDOEcdaaAlgorithm]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. Ecdaa AuthenticatorAttestationType = "ecdaa" + // AttCA - Indicates PrivacyCA attestation as defined in [TCG-CMCProfile-AIKCertEnroll]. Support for this attestation type is optional at this time. It might be required by FIDO Certification. AttCA AuthenticatorAttestationType = "attca" + // AnonCA In this case, the authenticator uses an Anonymization CA which dynamically generates per-credential attestation certificates such that the attestation statements presented to Relying Parties do not provide uniquely identifiable information, e.g., that might be used for tracking purposes. The applicable [WebAuthn] attestation formats "fmt" are Google SafetyNet Attestation "android-safetynet", Android Keystore Attestation "android-key", Apple Anonymous Attestation "apple", and Apple Application Attestation "apple-appattest". AnonCA AuthenticatorAttestationType = "anonca" + // None - Indicates absence of attestation None AuthenticatorAttestationType = "none" ) @@ -132,46 +137,98 @@ func IsUndesiredAuthenticatorStatus(status AuthenticatorStatus) bool { return false } +// IsUndesiredAuthenticatorStatusSlice returns whether the supplied authenticator status is desirable or not +func IsUndesiredAuthenticatorStatusSlice(status AuthenticatorStatus, values []AuthenticatorStatus) bool { + for _, s := range values { + if s == status { + return true + } + } + + return false +} + +// IsUndesiredAuthenticatorStatusMap returns whether the supplied authenticator status is desirable or not +func IsUndesiredAuthenticatorStatusMap(status AuthenticatorStatus, values map[AuthenticatorStatus]bool) bool { + _, ok := values[status] + + return ok +} + type AuthenticationAlgorithm string const ( - // An ECDSA signature on the NIST secp256r1 curve which must have raw R and S buffers, encoded in big-endian order. + // ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW is an ECDSA signature on the NIST secp256r1 curve which must have raw R and + // S buffers, encoded in big-endian order. ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_raw" - // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the NIST secp256r1 curve. + + // ALG_SIGN_SECP256R1_ECDSA_SHA256_DER is a DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the NIST secp256r1 + // curve. ALG_SIGN_SECP256R1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256r1_ecdsa_sha256_der" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + + // ALG_SIGN_RSASSA_PSS_SHA256_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian + // order RFC4055 RFC4056. ALG_SIGN_RSASSA_PSS_SHA256_RAW AuthenticationAlgorithm = "rsassa_pss_sha256_raw" - // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the RSASSA-PSS RFC3447 signature RFC4055 RFC4056. + + // ALG_SIGN_RSASSA_PSS_SHA256_DER is a DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the + // RSASSA-PSS RFC3447 signature RFC4055 RFC4056. ALG_SIGN_RSASSA_PSS_SHA256_DER AuthenticationAlgorithm = "rsassa_pss_sha256_der" - // An ECDSA signature on the secp256k1 curve which must have raw R and S buffers, encoded in big-endian order. + + // ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW is an ECDSA signature on the secp256k1 curve which must have raw R and S + // buffers, encoded in big-endian order. ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_raw" - // DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the secp256k1 curve. + + // ALG_SIGN_SECP256K1_ECDSA_SHA256_DER is a DER ITU-X690-2008 encoded ECDSA signature RFC5480 on the secp256k1 curve. ALG_SIGN_SECP256K1_ECDSA_SHA256_DER AuthenticationAlgorithm = "secp256k1_ecdsa_sha256_der" - // Chinese SM2 elliptic curve based signature algorithm combined with SM3 hash algorithm OSCCA-SM2 OSCCA-SM3. + + // ALG_SIGN_SM2_SM3_RAW is a Chinese SM2 elliptic curve based signature algorithm combined with SM3 hash algorithm + // OSCCA-SM2 OSCCA-SM3. ALG_SIGN_SM2_SM3_RAW AuthenticationAlgorithm = "sm2_sm3_raw" - // This is the EMSA-PKCS1-v1_5 signature as defined in RFC3447. + + // ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW is the EMSA-PKCS1-v1_5 signature as defined in RFC3447. ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_raw" - // DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the EMSA-PKCS1-v1_5 signature as defined in RFC3447. + + // ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER is a DER ITU-X690-2008 encoded OCTET STRING (not BIT STRING!) containing the + // EMSA-PKCS1-v1_5 signature as defined in RFC3447. ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER AuthenticationAlgorithm = "rsa_emsa_pkcs1_sha256_der" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + + // ALG_SIGN_RSASSA_PSS_SHA384_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian + // order RFC4055 RFC4056. ALG_SIGN_RSASSA_PSS_SHA384_RAW AuthenticationAlgorithm = "rsassa_pss_sha384_raw" - // RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian order RFC4055 RFC4056. + + // ALG_SIGN_RSASSA_PSS_SHA512_RAW is a RSASSA-PSS RFC3447 signature must have raw S buffers, encoded in big-endian + // order RFC4055 RFC4056. ALG_SIGN_RSASSA_PSS_SHA512_RAW AuthenticationAlgorithm = "rsassa_pss_sha512_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA256(aka RS256) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + + // ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA256(aka RS256) signature must have raw + // S buffers, encoded in big-endian order RFC8017 RFC4056 ALG_SIGN_RSASSA_PKCSV15_SHA256_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha256_raw" + // RSASSA-PKCS1-v1_5 RFC3447 with SHA384(aka RS384) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 ALG_SIGN_RSASSA_PKCSV15_SHA384_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha384_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA512(aka RS512) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + + // ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA512(aka RS512) signature must have raw + // S buffers, encoded in big-endian order RFC8017 RFC4056 ALG_SIGN_RSASSA_PKCSV15_SHA512_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha512_raw" - // RSASSA-PKCS1-v1_5 RFC3447 with SHA1(aka RS1) signature must have raw S buffers, encoded in big-endian order RFC8017 RFC4056 + + // ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW is a RSASSA-PKCS1-v1_5 RFC3447 with SHA1(aka RS1) signature must have raw S + // buffers, encoded in big-endian order RFC8017 RFC4056 ALG_SIGN_RSASSA_PKCSV15_SHA1_RAW AuthenticationAlgorithm = "rsassa_pkcsv15_sha1_raw" - // An ECDSA signature on the NIST secp384r1 curve with SHA384(aka: ES384) which must have raw R and S buffers, encoded in big-endian order. + + // ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW is an ECDSA signature on the NIST secp384r1 curve with SHA384(aka: ES384) + // which must have raw R and S buffers, encoded in big-endian order. ALG_SIGN_SECP384R1_ECDSA_SHA384_RAW AuthenticationAlgorithm = "secp384r1_ecdsa_sha384_raw" - // An ECDSA signature on the NIST secp512r1 curve with SHA512(aka: ES512) which must have raw R and S buffers, encoded in big-endian order. + + // ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW is an ECDSA signature on the NIST secp512r1 curve with SHA512(aka: ES512) + // which must have raw R and S buffers, encoded in big-endian order. ALG_SIGN_SECP521R1_ECDSA_SHA512_RAW AuthenticationAlgorithm = "secp521r1_ecdsa_sha512_raw" - // An EdDSA signature on the curve 25519, which must have raw R and S buffers, encoded in big-endian order. + + // ALG_SIGN_ED25519_EDDSA_SHA512_RAW is an EdDSA signature on the curve 25519, which must have raw R and S buffers, + // encoded in big-endian order. ALG_SIGN_ED25519_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed25519_eddsa_sha512_raw" - // An EdDSA signature on the curve Ed448, which must have raw R and S buffers, encoded in big-endian order. + + // ALG_SIGN_ED448_EDDSA_SHA512_RAW is an EdDSA signature on the curve Ed448, which must have raw R and S buffers, + // encoded in big-endian order. ALG_SIGN_ED448_EDDSA_SHA512_RAW AuthenticationAlgorithm = "ed448_eddsa_sha512_raw" ) @@ -220,23 +277,29 @@ func AlgKeyMatch(key algKeyCose, algs []AuthenticationAlgorithm) bool { type PublicKeyAlgAndEncoding string const ( - // Raw ANSI X9.62 formatted Elliptic Curve public key. + // ALG_KEY_ECC_X962_RAW is a raw ANSI X9.62 formatted Elliptic Curve public key. ALG_KEY_ECC_X962_RAW PublicKeyAlgAndEncoding = "ecc_x962_raw" - // DER ITU-X690-2008 encoded ANSI X.9.62 formatted SubjectPublicKeyInfo RFC5480 specifying an elliptic curve public key. + + // ALG_KEY_ECC_X962_DER is a DER ITU-X690-2008 encoded ANSI X.9.62 formatted SubjectPublicKeyInfo RFC5480 specifying an elliptic curve public key. ALG_KEY_ECC_X962_DER PublicKeyAlgAndEncoding = "ecc_x962_der" - // Raw encoded 2048-bit RSA public key RFC3447. + + // ALG_KEY_RSA_2048_RAW is a raw encoded 2048-bit RSA public key RFC3447. ALG_KEY_RSA_2048_RAW PublicKeyAlgAndEncoding = "rsa_2048_raw" - // ASN.1 DER [ITU-X690-2008] encoded 2048-bit RSA RFC3447 public key RFC4055. + + // ALG_KEY_RSA_2048_DER is a ASN.1 DER [ITU-X690-2008] encoded 2048-bit RSA RFC3447 public key RFC4055. ALG_KEY_RSA_2048_DER PublicKeyAlgAndEncoding = "rsa_2048_der" - // COSE_Key format, as defined in Section 7 of RFC8152. This encoding includes its own field for indicating the public key algorithm. + + // ALG_KEY_COSE is a COSE_Key format, as defined in Section 7 of RFC8152. This encoding includes its own field for indicating the public key algorithm. ALG_KEY_COSE PublicKeyAlgAndEncoding = "cose" ) type MetadataError struct { // Short name for the type of error that has occurred. Type string `json:"type"` + // Additional details about the error. Details string `json:"error"` + // Information to help debug the error. DevInfo string `json:"debug"` } From e83354ccf2787f787ee3f005086abe790652e827 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 9 Jun 2024 17:05:37 +1000 Subject: [PATCH 14/24] feat: cached --- metadata/metadata.go | 2 +- metadata/providers/cached/doc.go | 9 ++ metadata/providers/cached/options.go | 92 +++++++++++ metadata/providers/cached/provider.go | 146 ++++++++++++++++++ metadata/providers/cached/util.go | 50 ++++++ metadata/providers/memory/doc.go | 5 + .../providers/memory/{opts.go => options.go} | 34 ++-- metadata/providers/memory/provider.go | 19 ++- metadata/types.go | 15 ++ 9 files changed, 354 insertions(+), 18 deletions(-) create mode 100644 metadata/providers/cached/doc.go create mode 100644 metadata/providers/cached/options.go create mode 100644 metadata/providers/cached/provider.go create mode 100644 metadata/providers/cached/util.go create mode 100644 metadata/providers/memory/doc.go rename metadata/providers/memory/{opts.go => options.go} (77%) diff --git a/metadata/metadata.go b/metadata/metadata.go index ce81885..2f6b1f4 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -21,7 +21,7 @@ func Fetch() (metadata *Metadata, err error) { res *http.Response ) - if decoder, err = NewDecoder(); err != nil { + if decoder, err = NewDecoder(WithIgnoreEntryParsingErrors()); err != nil { return nil, err } diff --git a/metadata/providers/cached/doc.go b/metadata/providers/cached/doc.go new file mode 100644 index 0000000..ca370b4 --- /dev/null +++ b/metadata/providers/cached/doc.go @@ -0,0 +1,9 @@ +package cached + +// The cached package handles a metadata.Provider implementation that both downloads and caches the MDS3 blob. This +// effectively is the recommended provider in most instances as it's fairly robust. Alternatively we suggest +// implementing a similar provider that leverages the memory.Provider as an underlying element. +// +// This provider only specifically performs updates at the time it's initialized. It has no automatic update +// functionality. This may change in the future however if you want this functionality at this time we recommend making +// your own implementation. diff --git a/metadata/providers/cached/options.go b/metadata/providers/cached/options.go new file mode 100644 index 0000000..3abd763 --- /dev/null +++ b/metadata/providers/cached/options.go @@ -0,0 +1,92 @@ +package cached + +import ( + "net/http" + "net/url" + + "github.com/go-webauthn/webauthn/metadata" +) + +// Option describes an optional pattern for this provider. +type Option func(provider *Provider) (err error) + +// NewFunc describes the type used to create the underlying provider. +type NewFunc func(mds *metadata.Metadata) (provider metadata.Provider, err error) + +// WithPath sets the path name for the cached file. This option is REQUIRED. +func WithPath(name string) Option { + return func(provider *Provider) (err error) { + provider.name = name + + return nil + } +} + +// WithUpdate is used to enable or disable the update. By default it's set to true. +func WithUpdate(update bool) Option { + return func(provider *Provider) (err error) { + provider.update = update + + return nil + } +} + +// WithForceUpdate is used to force an update on creation. This will forcibly overwrite the file if possible. +func WithForceUpdate(force bool) Option { + return func(provider *Provider) (err error) { + provider.force = force + + return nil + } +} + +// WithNew customizes the NewFunc. By default we just create a fairly standard memory.Provider with strict defaults. +func WithNew(newup NewFunc) Option { + return func(provider *Provider) (err error) { + provider.newup = newup + + return nil + } +} + +// WithDecoder sets the decoder to be used for this provider. By default this is a decoder with the entry parsing errors +// configured to skip that entry. +func WithDecoder(decoder *metadata.Decoder) Option { + return func(provider *Provider) (err error) { + provider.decoder = decoder + + return nil + } +} + +// WithMetadataURL configures the URL to get the metadata from. This shouldn't be modified unless you know what you're +// doing as we use the metadata.ProductionMDSURL which is safe in most instances. +func WithMetadataURL(uri string) Option { + return func(provider *Provider) (err error) { + if _, err = url.ParseRequestURI(uri); err != nil { + return err + } + + provider.uri = uri + + return nil + } +} + +// WithClient configures the *http.Client used to get the MDS3 blob. +func WithClient(client *http.Client) Option { + return func(provider *Provider) (err error) { + provider.client = client + + return nil + } +} + +// WithClock allows injection of a metadata.Clock to check the up-to-date status of a blob. +func WithClock(clock metadata.Clock) Option { + return func(provider *Provider) (err error) { + provider.clock = clock + + return nil + } +} diff --git a/metadata/providers/cached/provider.go b/metadata/providers/cached/provider.go new file mode 100644 index 0000000..ad38938 --- /dev/null +++ b/metadata/providers/cached/provider.go @@ -0,0 +1,146 @@ +package cached + +import ( + "fmt" + "io" + "net/http" + "os" + + "github.com/go-webauthn/webauthn/metadata" +) + +// New returns a new cached Provider given a set of functional Option's. This provider will download a new version and +// save it to the configured file path if it doesn't exist or if it's out of date by default. +func New(opts ...Option) (provider metadata.Provider, err error) { + p := &Provider{ + update: true, + uri: metadata.ProductionMDSURL, + } + + for _, opt := range opts { + if err = opt(p); err != nil { + return nil, err + } + } + + if p.name == "" { + return nil, fmt.Errorf("provider configured without setting a path for the cached file blob") + } + + if p.newup == nil { + p.newup = defaultNew + } + + if p.decoder == nil { + if p.decoder, err = metadata.NewDecoder(metadata.WithIgnoreEntryParsingErrors()); err != nil { + return nil, err + } + } + + if p.clock == nil { + p.clock = &metadata.RealClock{} + } + + if err = p.init(); err != nil { + return nil, err + } + + return p, nil +} + +// Provider implements a metadata.Provider with a file-based cache. +type Provider struct { + metadata.Provider + + name string + uri string + update bool + force bool + clock metadata.Clock + client *http.Client + decoder *metadata.Decoder + newup NewFunc +} + +func (p *Provider) init() (err error) { + var ( + f *os.File + rc io.ReadCloser + created bool + mds *metadata.Metadata + ) + + if f, created, err = doOpenOrCreate(p.name); err != nil { + return err + } + + defer f.Close() + + if created || p.force { + if rc, err = p.get(); err != nil { + return err + } + } else { + if mds, err = p.parse(f); err != nil { + return err + } + + if p.outdated(mds) { + if rc, err = p.get(); err != nil { + return err + } + } + } + + if rc != nil { + if err = doTruncateCopyAndSeekStart(f, rc); err != nil { + return err + } + + if mds, err = p.parse(f); err != nil { + return err + } + } + + var provider metadata.Provider + + if provider, err = p.newup(mds); err != nil { + return err + } + + p.Provider = provider + + return nil +} + +func (p *Provider) parse(rc io.ReadCloser) (data *metadata.Metadata, err error) { + var payload *metadata.MetadataBLOBPayloadJSON + + if payload, err = p.decoder.Decode(rc); err != nil { + return nil, err + } + + if data, err = p.decoder.Parse(payload); err != nil { + return nil, err + } + + return data, nil +} + +func (p *Provider) outdated(mds *metadata.Metadata) bool { + return p.update && p.clock.Now().After(mds.Parsed.NextUpdate) +} + +func (p *Provider) get() (f io.ReadCloser, err error) { + if p.client == nil { + p.client = &http.Client{} + } + + var res *http.Response + + if res, err = p.client.Get(p.uri); err != nil { + return nil, err + } + + return res.Body, nil +} diff --git a/metadata/providers/cached/util.go b/metadata/providers/cached/util.go new file mode 100644 index 0000000..7d401ac --- /dev/null +++ b/metadata/providers/cached/util.go @@ -0,0 +1,50 @@ +package cached + +import ( + "github.com/go-webauthn/webauthn/metadata" + "github.com/go-webauthn/webauthn/metadata/providers/memory" + "io" + "os" +) + +func doTruncateCopyAndSeekStart(f *os.File, rc io.ReadCloser) (err error) { + if err = f.Truncate(0); err != nil { + return err + } + + if _, err = io.Copy(f, rc); err != nil { + return err + } + + if _, err = f.Seek(0, io.SeekStart); err != nil { + return err + } + + return rc.Close() +} + +func doOpenOrCreate(name string) (f *os.File, created bool, err error) { + if f, err = os.Open(name); err == nil { + return f, false, nil + } + + if os.IsNotExist(err) { + if f, err = os.Create(name); err != nil { + return nil, false, err + } + + return f, true, nil + } + + return nil, false, err +} + +func defaultNew(mds *metadata.Metadata) (provider metadata.Provider, err error) { + return memory.New( + memory.WithMetadata(mds.ToMap()), + memory.WithValidateEntry(true), + memory.WithValidateEntryPermitZeroAAGUID(false), + memory.WithValidateTrustAnchor(true), + memory.WithValidateStatus(true), + ) +} diff --git a/metadata/providers/memory/doc.go b/metadata/providers/memory/doc.go new file mode 100644 index 0000000..95c077d --- /dev/null +++ b/metadata/providers/memory/doc.go @@ -0,0 +1,5 @@ +package memory + +// The memory package handles a metadata.Provider implementation that solely exists in memory. It's intended as a basis +// for other providers and generally not recommended to use directly unless you're implementing your own logic to handle +// the download and potential caching of the MDS3 blob yourself. diff --git a/metadata/providers/memory/opts.go b/metadata/providers/memory/options.go similarity index 77% rename from metadata/providers/memory/opts.go rename to metadata/providers/memory/options.go index 4d9f8fc..1281e2f 100644 --- a/metadata/providers/memory/opts.go +++ b/metadata/providers/memory/options.go @@ -7,12 +7,14 @@ import ( ) // Option describes an optional pattern for this provider. -type Option func(*Provider) +type Option func(provider *Provider) (err error) // WithMetadata provides the required metadata for the memory provider. -func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option { - return func(provider *Provider) { - provider.mds = metadata +func WithMetadata(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option { + return func(provider *Provider) (err error) { + provider.mds = mds + + return nil } } @@ -20,47 +22,59 @@ func WithMetadata(metadata map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Opt // valid. By default an AAGUID which has a zero value should fail validation if WithValidateEntryPermitZeroAAGUID is not // provided with the value of true. func WithValidateEntry(require bool) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.entry = require + + return nil } } // WithValidateEntryPermitZeroAAGUID is an option that permits a zero'd AAGUID from an attestation statement to // automatically pass metadata validations. Generally helpful to use with WithValidateEntry. func WithValidateEntryPermitZeroAAGUID(permit bool) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.entryPermitZero = permit + + return nil } } // WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor // from the metadata. func WithValidateTrustAnchor(validate bool) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.anchors = validate + + return nil } } // WithValidateStatus when set to true enables the validation of the attestation statments AAGUID against the desired // and undesired metadata.AuthenticatorStatus lists. func WithValidateStatus(validate bool) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.status = validate + + return nil } } // WithStatusUndesired provides the list of statuses which are considered undesirable for status report validation // purposes. Should be used with WithValidateStatus set to true. func WithStatusUndesired(statuses []metadata.AuthenticatorStatus) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.undesired = statuses + + return nil } } // WithStatusDesired provides the list of statuses which are considered desired and will be required for status report // validation purposes. Should be used with WithValidateStatus set to true. func WithStatusDesired(statuses []metadata.AuthenticatorStatus) Option { - return func(provider *Provider) { + return func(provider *Provider) (err error) { provider.desired = statuses + + return nil } } diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index e60a080..f935c0a 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -2,24 +2,29 @@ package memory import ( "context" - + "fmt" "github.com/google/uuid" "github.com/go-webauthn/webauthn/metadata" ) -// New returns a new memory provider given a set of functional Option's. -func New(opts ...Option) (provider *Provider) { - - provider = &Provider{ +// New returns a new memory Provider given a set of functional Option's. +func New(opts ...Option) (provider metadata.Provider, err error) { + p := &Provider{ undesired: metadata.DefaultUndesiredAuthenticatorStatuses(), } for _, opt := range opts { - opt(provider) + if err = opt(p); err != nil { + return nil, err + } + } + + if p.mds == nil { + return nil, fmt.Errorf("memory metadata provider has not been initialized with metadata") } - return provider + return p, nil } // Provider is a concrete implementation of the metadata.Provider that utilizes memory for validation. This provider is diff --git a/metadata/types.go b/metadata/types.go index 84474b0..72cb84a 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -4,6 +4,7 @@ import ( "context" "errors" "reflect" + "time" "github.com/google/uuid" @@ -307,3 +308,17 @@ type MetadataError struct { func (err *MetadataError) Error() string { return err.Details } + +// Clock is an interface used to implement clock functionality in various metadata areas. +type Clock interface { + // Now returns the current time. + Now() time.Time +} + +// RealClock is just a real clock. +type RealClock struct{} + +// Now returns the current time. +func (RealClock) Now() time.Time { + return time.Now() +} From 427baacc92e38a54f246f7a182d423b43060b185 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 10 Jun 2024 08:55:00 +1000 Subject: [PATCH 15/24] feat: refactor --- metadata/decode.go | 18 +++---- metadata/metadata.go | 70 +++++++++++++------------- metadata/metadata_test.go | 12 ++--- metadata/providers/cached/provider.go | 2 +- metadata/providers/memory/options.go | 2 +- metadata/providers/memory/provider.go | 4 +- metadata/types.go | 2 +- protocol/attestation.go | 2 +- protocol/metadata.go | 44 +++++++++++++++++ webauthn/login.go | 71 +++++++++++++++++---------- webauthn/registration.go | 2 +- webauthn/types.go | 5 +- 12 files changed, 149 insertions(+), 85 deletions(-) create mode 100644 protocol/metadata.go diff --git a/metadata/decode.go b/metadata/decode.go index db03a09..f290012 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -46,9 +46,9 @@ type Decoder struct { } // Parse handles parsing of the raw JSON values of the metadata blob. Should be used after using Decode or DecodeBytes. -func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, err error) { +func (d *Decoder) Parse(payload *PayloadJSON) (metadata *Metadata, err error) { metadata = &Metadata{ - Parsed: MetadataBLOBPayload{ + Parsed: Parsed{ LegalHeader: payload.LegalHeader, Number: payload.Number, }, @@ -58,13 +58,13 @@ func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, e return nil, fmt.Errorf("error occurred parsing next update value '%s': %w", payload.NextUpdate, err) } - var parsed MetadataBLOBPayloadEntry + var parsed Entry for _, entry := range payload.Entries { if parsed, err = entry.Parse(); err != nil { - metadata.Unparsed = append(metadata.Unparsed, MetadataBLOBPayloadEntryError{ - Error: err, - MetadataBLOBPayloadEntryJSON: entry, + metadata.Unparsed = append(metadata.Unparsed, EntryError{ + Error: err, + EntryJSON: entry, }) continue @@ -81,7 +81,7 @@ func (d *Decoder) Parse(payload *MetadataBLOBPayloadJSON) (metadata *Metadata, e } // Decode the blob from an io.ReadCloser. This function will close the io.ReadCloser after completing. -func (d *Decoder) Decode(r io.ReadCloser) (payload *MetadataBLOBPayloadJSON, err error) { +func (d *Decoder) Decode(r io.ReadCloser) (payload *PayloadJSON, err error) { defer r.Close() bytes, err := io.ReadAll(r) @@ -93,8 +93,8 @@ func (d *Decoder) Decode(r io.ReadCloser) (payload *MetadataBLOBPayloadJSON, err } // DecodeBytes handles decoding raw bytes. If you have a read closer it's suggested to use Decode. -func (d *Decoder) DecodeBytes(bytes []byte) (payload *MetadataBLOBPayloadJSON, err error) { - payload = &MetadataBLOBPayloadJSON{} +func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) { + payload = &PayloadJSON{} var token *jwt.Token diff --git a/metadata/metadata.go b/metadata/metadata.go index 2f6b1f4..58dfb4b 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -17,7 +17,7 @@ import ( func Fetch() (metadata *Metadata, err error) { var ( decoder *Decoder - payload *MetadataBLOBPayloadJSON + payload *PayloadJSON res *http.Response ) @@ -39,12 +39,12 @@ func Fetch() (metadata *Metadata, err error) { } type Metadata struct { - Parsed MetadataBLOBPayload - Unparsed []MetadataBLOBPayloadEntryError + Parsed Parsed + Unparsed []EntryError } -func (m *Metadata) ToMap() (metadata map[uuid.UUID]*MetadataBLOBPayloadEntry) { - metadata = make(map[uuid.UUID]*MetadataBLOBPayloadEntry) +func (m *Metadata) ToMap() (metadata map[uuid.UUID]*Entry) { + metadata = make(map[uuid.UUID]*Entry) for _, entry := range m.Parsed.Entries { if entry.AaGUID != uuid.Nil { @@ -55,15 +55,10 @@ func (m *Metadata) ToMap() (metadata map[uuid.UUID]*MetadataBLOBPayloadEntry) { return metadata } -type MetadataBLOBPayloadEntryError struct { - Error error - MetadataBLOBPayloadEntryJSON -} - -// MetadataBLOBPayload is a structure representing the MetadataBLOBPayload MDS3 dictionary. +// Parsed is a structure representing the Parsed MDS3 dictionary. // // See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary -type MetadataBLOBPayload struct { +type Parsed struct { // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. LegalHeader string @@ -74,19 +69,19 @@ type MetadataBLOBPayload struct { NextUpdate time.Time // List of zero or more MetadataTOCPayloadEntry objects. - Entries []MetadataBLOBPayloadEntry + Entries []Entry } -// MetadataBLOBPayloadJSON is an intermediary JSON/JWT representation of the MetadataBLOBPayload. -type MetadataBLOBPayloadJSON struct { +// PayloadJSON is an intermediary JSON/JWT representation of the Parsed. +type PayloadJSON struct { LegalHeader string `json:"legalHeader"` Number int `json:"no"` NextUpdate string `json:"nextUpdate"` - Entries []MetadataBLOBPayloadEntryJSON `json:"entries"` + Entries []EntryJSON `json:"entries"` } -func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error) { +func (j PayloadJSON) Parse() (payload Parsed, err error) { var update time.Time if update, err = time.Parse(time.DateOnly, j.NextUpdate); err != nil { @@ -95,7 +90,7 @@ func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error n := len(j.Entries) - entries := make([]MetadataBLOBPayloadEntry, n) + entries := make([]Entry, n) for i := 0; i < n; i++ { if entries[i], err = j.Entries[i].Parse(); err != nil { @@ -103,7 +98,7 @@ func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error } } - return MetadataBLOBPayload{ + return Parsed{ LegalHeader: j.LegalHeader, Number: j.Number, NextUpdate: update, @@ -111,10 +106,10 @@ func (j MetadataBLOBPayloadJSON) Parse() (payload MetadataBLOBPayload, err error }, nil } -// MetadataBLOBPayloadEntry is a structure representing the MetadataBLOBPayloadEntry MDS3 dictionary. +// Entry is a structure representing the Entry MDS3 dictionary. // // See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary -type MetadataBLOBPayloadEntry struct { +type Entry struct { // The Authenticator Attestation ID. Aaid string `json:"aaid"` @@ -125,7 +120,7 @@ type MetadataBLOBPayloadEntry struct { AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` // The metadataStatement JSON object as defined in FIDOMetadataStatement. - MetadataStatement MetadataStatement `json:"metadataStatement"` + MetadataStatement Statement `json:"metadataStatement"` // Status of the FIDO Biometric Certification of one or more biometric components of the Authenticator BiometricStatusReports []BiometricStatusReport `json:"biometricStatusReports"` @@ -143,15 +138,15 @@ type MetadataBLOBPayloadEntry struct { RogueListHash string } -// MetadataBLOBPayloadEntryJSON is an intermediary JSON/JWT structure representing the MetadataBLOBPayloadEntry MDS3 dictionary. +// EntryJSON is an intermediary JSON/JWT structure representing the Entry MDS3 dictionary. // // See: https://fidoalliance.org/specs/mds/fido-metadata-service-v3.0-ps-20210518.html#metadata-blob-payload-entry-dictionary -type MetadataBLOBPayloadEntryJSON struct { +type EntryJSON struct { Aaid string `json:"aaid"` AaGUID string `json:"aaguid"` AttestationCertificateKeyIdentifiers []string `json:"attestationCertificateKeyIdentifiers"` - MetadataStatement MetadataStatementJSON `json:"metadataStatement"` + MetadataStatement StatementJSON `json:"metadataStatement"` BiometricStatusReports []BiometricStatusReportJSON `json:"biometricStatusReports"` StatusReports []StatusReportJSON `json:"statusReports"` @@ -160,7 +155,7 @@ type MetadataBLOBPayloadEntryJSON struct { RogueListHash string `json:"rogueListHash"` } -func (j MetadataBLOBPayloadEntryJSON) Parse() (entry MetadataBLOBPayloadEntry, err error) { +func (j EntryJSON) Parse() (entry Entry, err error) { var aaguid uuid.UUID if len(j.AaGUID) != 0 { @@ -169,7 +164,7 @@ func (j MetadataBLOBPayloadEntryJSON) Parse() (entry MetadataBLOBPayloadEntry, e } } - var statement MetadataStatement + var statement Statement if statement, err = j.MetadataStatement.Parse(); err != nil { return entry, fmt.Errorf("error occurred parsing metadata entry with AAGUID '%s': %w", j.AaGUID, err) @@ -215,7 +210,7 @@ func (j MetadataBLOBPayloadEntryJSON) Parse() (entry MetadataBLOBPayloadEntry, e } } - return MetadataBLOBPayloadEntry{ + return Entry{ Aaid: j.Aaid, AaGUID: aaguid, AttestationCertificateKeyIdentifiers: j.AttestationCertificateKeyIdentifiers, @@ -228,12 +223,12 @@ func (j MetadataBLOBPayloadEntryJSON) Parse() (entry MetadataBLOBPayloadEntry, e }, nil } -// MetadataStatement is a structure representing the MetadataStatement MDS3 dictionary. +// Statement is a structure representing the Statement MDS3 dictionary. // Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information // contained in the authoritative statement is used in several other places. // // See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#metadata-keys -type MetadataStatement struct { +type Statement struct { // The legalHeader, if present, contains a legal guide for accessing and using metadata, which itself MAY contain URL(s) pointing to further information, such as a full Terms and Conditions statement. LegalHeader string @@ -321,7 +316,7 @@ type MetadataStatement struct { AuthenticatorGetInfo AuthenticatorGetInfo } -func (s *MetadataStatement) Verifier() (opts x509.VerifyOptions) { +func (s *Statement) Verifier() (opts x509.VerifyOptions) { roots := x509.NewCertPool() for _, root := range s.AttestationRootCertificates { @@ -333,12 +328,12 @@ func (s *MetadataStatement) Verifier() (opts x509.VerifyOptions) { } } -// MetadataStatementJSON is an intermediary JSON/JWT structure representing the MetadataStatement MDS3 dictionary. +// StatementJSON is an intermediary JSON/JWT structure representing the Statement MDS3 dictionary. // Authenticator metadata statements are used directly by the FIDO server at a relying party, but the information // contained in the authoritative statement is used in several other places. // // See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#metadata-keys -type MetadataStatementJSON struct { +type StatementJSON struct { LegalHeader string `json:"legalHeader"` Aaid string `json:"aaid"` AaGUID string `json:"aaguid"` @@ -368,7 +363,7 @@ type MetadataStatementJSON struct { AuthenticatorGetInfo AuthenticatorGetInfoJSON `json:"authenticatorGetInfo"` } -func (j MetadataStatementJSON) Parse() (statement MetadataStatement, err error) { +func (j StatementJSON) Parse() (statement Statement, err error) { var aaguid uuid.UUID if len(j.AaGUID) != 0 { @@ -401,7 +396,7 @@ func (j MetadataStatementJSON) Parse() (statement MetadataStatement, err error) return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing authenticator get info value: %w", j.Description, err) } - return MetadataStatement{ + return Statement{ LegalHeader: j.LegalHeader, Aaid: j.Aaid, AaGUID: aaguid, @@ -914,3 +909,8 @@ func DefaultUndesiredAuthenticatorStatuses() []AuthenticatorStatus { return undesired } + +type EntryError struct { + Error error + EntryJSON +} diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index cadecb3..eeca71a 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -81,11 +81,11 @@ func TestConformanceMetadataTOCParsing(t *testing.T) { require.NoError(t, err) - metadata := make(map[uuid.UUID]MetadataBLOBPayloadEntryJSON) + metadata := make(map[uuid.UUID]EntryJSON) var ( res *http.Response - blob *MetadataBLOBPayloadJSON + blob *PayloadJSON me *MetadataError ) @@ -317,8 +317,8 @@ func getEndpoints(c *http.Client) ([]string, error) { return resp.Result, err } -func getTestMetadata(s string, c *http.Client) (MetadataStatementJSON, error) { - var statement MetadataStatementJSON +func getTestMetadata(s string, c *http.Client) (StatementJSON, error) { + var statement StatementJSON // MDSGetEndpointsRequest is the request sent to the conformance metadata getEndpoints endpoint. type MDSGetTestMetadata struct { @@ -345,8 +345,8 @@ func getTestMetadata(s string, c *http.Client) (MetadataStatementJSON, error) { } type ConformanceResponse struct { - Status string `json:"status"` - Result MetadataStatementJSON `json:"result"` + Status string `json:"status"` + Result StatementJSON `json:"result"` } var resp ConformanceResponse diff --git a/metadata/providers/cached/provider.go b/metadata/providers/cached/provider.go index ad38938..d8a0dd1 100644 --- a/metadata/providers/cached/provider.go +++ b/metadata/providers/cached/provider.go @@ -114,7 +114,7 @@ func (p *Provider) init() (err error) { } func (p *Provider) parse(rc io.ReadCloser) (data *metadata.Metadata, err error) { - var payload *metadata.MetadataBLOBPayloadJSON + var payload *metadata.PayloadJSON if payload, err = p.decoder.Decode(rc); err != nil { return nil, err diff --git a/metadata/providers/memory/options.go b/metadata/providers/memory/options.go index 1281e2f..7089bed 100644 --- a/metadata/providers/memory/options.go +++ b/metadata/providers/memory/options.go @@ -10,7 +10,7 @@ import ( type Option func(provider *Provider) (err error) // WithMetadata provides the required metadata for the memory provider. -func WithMetadata(mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry) Option { +func WithMetadata(mds map[uuid.UUID]*metadata.Entry) Option { return func(provider *Provider) (err error) { provider.mds = mds diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index f935c0a..052b7e9 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -31,7 +31,7 @@ func New(opts ...Option) (provider metadata.Provider, err error) { // a simple one-shot that doesn't perform any locking, provide dynamic functionality, or download the metadata at any // stage (it expects it's provided via one of the Option's). type Provider struct { - mds map[uuid.UUID]*metadata.MetadataBLOBPayloadEntry + mds map[uuid.UUID]*metadata.Entry desired []metadata.AuthenticatorStatus undesired []metadata.AuthenticatorStatus entry bool @@ -40,7 +40,7 @@ type Provider struct { status bool } -func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.MetadataBLOBPayloadEntry, err error) { +func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.Entry, err error) { if p.mds == nil { return nil, metadata.ErrNotInitialized } diff --git a/metadata/types.go b/metadata/types.go index 72cb84a..daf5f1a 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -13,7 +13,7 @@ import ( type Provider interface { // GetEntry returns a MDS3 payload entry given a AAGUID. This - GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *MetadataBLOBPayloadEntry, err error) + GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *Entry, err error) // GetValidateEntry returns true if this provider requires an entry to exist with a AAGUID matching the attestation // statement during registration. diff --git a/protocol/attestation.go b/protocol/attestation.go index 90979e3..fc8f0f2 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -173,7 +173,7 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat var ( aaguid uuid.UUID - entry *metadata.MetadataBLOBPayloadEntry + entry *metadata.Entry ) if len(a.AuthData.AttData.AAGUID) != 0 { diff --git a/protocol/metadata.go b/protocol/metadata.go new file mode 100644 index 0000000..a7e6651 --- /dev/null +++ b/protocol/metadata.go @@ -0,0 +1,44 @@ +package protocol + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/go-webauthn/webauthn/metadata" +) + +func ValidateMetadata(ctx context.Context, aaguid uuid.UUID, mds metadata.Provider) (err error) { + if mds == nil { + return nil + } + + var ( + entry *metadata.Entry + ) + + if entry, err = mds.GetEntry(ctx, aaguid); err != nil { + return err + } + + if entry == nil { + if aaguid == uuid.Nil && mds.GetValidateEntryPermitZeroAAGUID(ctx) { + return nil + } + + if mds.GetValidateEntry(ctx) { + return fmt.Errorf("error occurred performing authenticator entry validation: AAGUID entry has not been registered with the metadata service") + } + + return nil + } + + if mds.GetValidateStatus(ctx) { + if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil { + return fmt.Errorf("error occurred performing authenticator status validation: %w", err) + } + } + + return nil +} diff --git a/webauthn/login.go b/webauthn/login.go index bbff80d..d5a01b3 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -2,11 +2,14 @@ package webauthn import ( "bytes" + "context" "fmt" "net/http" "net/url" "time" + "github.com/google/uuid" + "github.com/go-webauthn/webauthn/protocol" ) @@ -222,16 +225,19 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe // allowCredentials. // NON-NORMATIVE Prior Step: Verify that the allowCredentials for the session are owned by the user provided. - userCredentials := user.WebAuthnCredentials() + credentials := user.WebAuthnCredentials() - var credentialFound bool + var ( + found bool + credential Credential + ) if len(session.AllowedCredentialIDs) > 0 { var credentialsOwned bool for _, allowedCredentialID := range session.AllowedCredentialIDs { - for _, userCredential := range userCredentials { - if bytes.Equal(userCredential.ID, allowedCredentialID) { + for _, credential = range credentials { + if bytes.Equal(credential.ID, allowedCredentialID) { credentialsOwned = true break @@ -247,13 +253,13 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe for _, allowedCredentialID := range session.AllowedCredentialIDs { if bytes.Equal(parsedResponse.RawID, allowedCredentialID) { - credentialFound = true + found = true break } } - if !credentialFound { + if !found { return nil, protocol.ErrBadRequest.WithDetails("User does not own the credential returned") } } @@ -272,44 +278,57 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe // Step 3. Using credential’s id attribute (or the corresponding rawId, if base64url encoding is inappropriate // for your use case), look up the corresponding credential public key. - var loginCredential Credential - - for _, cred := range userCredentials { - if bytes.Equal(cred.ID, parsedResponse.RawID) { - loginCredential = cred - credentialFound = true + for _, credential = range credentials { + if bytes.Equal(credential.ID, parsedResponse.RawID) { + found = true break } - credentialFound = false + found = false } - if !credentialFound { + if !found { return nil, protocol.ErrBadRequest.WithDetails("Unable to find the credential for the returned credential ID") } + var ( + appID string + err error + ) + + // Ensure authenticators with a bad status is not used. + if webauthn.Config.MDS != nil { + var aaguid uuid.UUID + + if aaguid, err = uuid.FromBytes(credential.Authenticator.AAGUID); err != nil { + return nil, protocol.ErrBadRequest.WithDetails("Failed to decode AAGUID").WithInfo(fmt.Sprintf("Error occurred decoding AAGUID from the credential record: %s", err)) + } + + if err = protocol.ValidateMetadata(context.Background(), aaguid, webauthn.Config.MDS); err != nil { + return nil, protocol.ErrBadRequest.WithDetails("Failed to validate credential record metadata").WithInfo(fmt.Sprintf("Error occurred validating authenticator metadata from the credential record: %s", err)) + } + } + shouldVerifyUser := session.UserVerification == protocol.VerificationRequired rpID := webauthn.Config.RPID rpOrigins := webauthn.Config.RPOrigins rpTopOrigins := webauthn.Config.RPTopOrigins - appID, err := parsedResponse.GetAppID(session.Extensions, loginCredential.AttestationType) - if err != nil { + if appID, err = parsedResponse.GetAppID(session.Extensions, credential.AttestationType); err != nil { return nil, err } // Handle steps 4 through 16. - validError := parsedResponse.Verify(session.Challenge, rpID, rpOrigins, rpTopOrigins, webauthn.Config.RPTopOriginVerificationMode, appID, shouldVerifyUser, loginCredential.PublicKey) - if validError != nil { - return nil, validError + if err = parsedResponse.Verify(session.Challenge, rpID, rpOrigins, rpTopOrigins, webauthn.Config.RPTopOriginVerificationMode, appID, shouldVerifyUser, credential.PublicKey); err != nil { + return nil, err } // Handle step 17. - loginCredential.Authenticator.UpdateCounter(parsedResponse.Response.AuthenticatorData.Counter) + credential.Authenticator.UpdateCounter(parsedResponse.Response.AuthenticatorData.Counter) // Check if the BackupEligible flag has changed. - if loginCredential.Flags.BackupEligible != parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() { + if credential.Flags.BackupEligible != parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() { return nil, protocol.ErrBadRequest.WithDetails("BackupEligible flag inconsistency detected during login validation") } @@ -319,10 +338,10 @@ func (webauthn *WebAuthn) validateLogin(user User, session SessionData, parsedRe } // Update flags from response data. - loginCredential.Flags.UserPresent = parsedResponse.Response.AuthenticatorData.Flags.HasUserPresent() - loginCredential.Flags.UserVerified = parsedResponse.Response.AuthenticatorData.Flags.HasUserVerified() - loginCredential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() - loginCredential.Flags.BackupState = parsedResponse.Response.AuthenticatorData.Flags.HasBackupState() + credential.Flags.UserPresent = parsedResponse.Response.AuthenticatorData.Flags.HasUserPresent() + credential.Flags.UserVerified = parsedResponse.Response.AuthenticatorData.Flags.HasUserVerified() + credential.Flags.BackupEligible = parsedResponse.Response.AuthenticatorData.Flags.HasBackupEligible() + credential.Flags.BackupState = parsedResponse.Response.AuthenticatorData.Flags.HasBackupState() - return &loginCredential, nil + return &credential, nil } diff --git a/webauthn/registration.go b/webauthn/registration.go index 3352eb2..79590da 100644 --- a/webauthn/registration.go +++ b/webauthn/registration.go @@ -227,7 +227,7 @@ func (webauthn *WebAuthn) CreateCredential(user User, session SessionData, parse var clientDataHash []byte - if clientDataHash, err = parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.MetaData); err != nil { + if clientDataHash, err = parsedResponse.Verify(session.Challenge, shouldVerifyUser, webauthn.Config.RPID, webauthn.Config.RPOrigins, webauthn.Config.RPTopOrigins, webauthn.Config.RPTopOriginVerificationMode, webauthn.Config.MDS); err != nil { return nil, err } diff --git a/webauthn/types.go b/webauthn/types.go index 24e3faf..5ce2f93 100644 --- a/webauthn/types.go +++ b/webauthn/types.go @@ -63,7 +63,8 @@ type Config struct { // Timeouts configures various timeouts. Timeouts TimeoutsConfig - MetaData metadata.Provider + // MDS is a metadata.Provider and enables various metadata validations if configured. + MDS metadata.Provider validated bool } @@ -156,7 +157,7 @@ func (c *Config) GetTopOriginVerificationMode() protocol.TopOriginVerificationMo } func (c *Config) GetMetaDataProvider() metadata.Provider { - return c.MetaData + return c.MDS } type ConfigProvider interface { From eb9b03d9761d02b7c6ac1718c2500928b52e8b2b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 10 Jun 2024 09:14:00 +1000 Subject: [PATCH 16/24] feat: refactor --- metadata/decode.go | 2 +- metadata/doc.go | 2 ++ metadata/metadata.go | 2 +- metadata/providers/cached/doc.go | 5 ++--- metadata/providers/cached/util.go | 5 +++-- metadata/providers/memory/doc.go | 5 ++--- metadata/providers/memory/provider.go | 1 + protocol/attestation_tpm_test.go | 2 +- protocol/base64_test.go | 5 +++-- 9 files changed, 16 insertions(+), 13 deletions(-) create mode 100644 metadata/doc.go diff --git a/metadata/decode.go b/metadata/decode.go index f290012..896e417 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -74,7 +74,7 @@ func (d *Decoder) Parse(payload *PayloadJSON) (metadata *Metadata, err error) { } if n := len(metadata.Unparsed); n != 0 && !d.ignoreEntryParsingErrors { - return metadata, fmt.Errorf("error occured parsing metadata: %d entries had errors during parsing", n) + return metadata, fmt.Errorf("error occurred parsing metadata: %d entries had errors during parsing", n) } return metadata, nil diff --git a/metadata/doc.go b/metadata/doc.go new file mode 100644 index 0000000..7db8c71 --- /dev/null +++ b/metadata/doc.go @@ -0,0 +1,2 @@ +// Package metadata handles metadata validation instrumentation. +package metadata diff --git a/metadata/metadata.go b/metadata/metadata.go index 58dfb4b..194a6fe 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -368,7 +368,7 @@ func (j StatementJSON) Parse() (statement Statement, err error) { if len(j.AaGUID) != 0 { if aaguid, err = uuid.Parse(j.AaGUID); err != nil { - return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occured parsing AAGUID value: %w", j.Description, err) + return statement, fmt.Errorf("error occurred parsing statement with description '%s': error occurred parsing AAGUID value: %w", j.Description, err) } } diff --git a/metadata/providers/cached/doc.go b/metadata/providers/cached/doc.go index ca370b4..cb18e91 100644 --- a/metadata/providers/cached/doc.go +++ b/metadata/providers/cached/doc.go @@ -1,9 +1,8 @@ -package cached - -// The cached package handles a metadata.Provider implementation that both downloads and caches the MDS3 blob. This +// Package cached handles a metadata.Provider implementation that both downloads and caches the MDS3 blob. This // effectively is the recommended provider in most instances as it's fairly robust. Alternatively we suggest // implementing a similar provider that leverages the memory.Provider as an underlying element. // // This provider only specifically performs updates at the time it's initialized. It has no automatic update // functionality. This may change in the future however if you want this functionality at this time we recommend making // your own implementation. +package cached diff --git a/metadata/providers/cached/util.go b/metadata/providers/cached/util.go index 7d401ac..98d7dad 100644 --- a/metadata/providers/cached/util.go +++ b/metadata/providers/cached/util.go @@ -1,10 +1,11 @@ package cached import ( - "github.com/go-webauthn/webauthn/metadata" - "github.com/go-webauthn/webauthn/metadata/providers/memory" "io" "os" + + "github.com/go-webauthn/webauthn/metadata" + "github.com/go-webauthn/webauthn/metadata/providers/memory" ) func doTruncateCopyAndSeekStart(f *os.File, rc io.ReadCloser) (err error) { diff --git a/metadata/providers/memory/doc.go b/metadata/providers/memory/doc.go index 95c077d..bc2679f 100644 --- a/metadata/providers/memory/doc.go +++ b/metadata/providers/memory/doc.go @@ -1,5 +1,4 @@ -package memory - -// The memory package handles a metadata.Provider implementation that solely exists in memory. It's intended as a basis +// Package memory handles a metadata.Provider implementation that solely exists in memory. It's intended as a basis // for other providers and generally not recommended to use directly unless you're implementing your own logic to handle // the download and potential caching of the MDS3 blob yourself. +package memory diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index 052b7e9..7eef9da 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -3,6 +3,7 @@ package memory import ( "context" "fmt" + "github.com/google/uuid" "github.com/go-webauthn/webauthn/metadata" diff --git a/protocol/attestation_tpm_test.go b/protocol/attestation_tpm_test.go index 3f33763..d8f6a4e 100644 --- a/protocol/attestation_tpm_test.go +++ b/protocol/attestation_tpm_test.go @@ -11,12 +11,12 @@ import ( "crypto/x509/pkix" "encoding/asn1" "encoding/binary" - "github.com/stretchr/testify/require" "math/big" "testing" "github.com/google/go-tpm/legacy/tpm2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/go-webauthn/webauthn/protocol/webauthncbor" "github.com/go-webauthn/webauthn/protocol/webauthncose" diff --git a/protocol/base64_test.go b/protocol/base64_test.go index 2540a54..6ec2132 100644 --- a/protocol/base64_test.go +++ b/protocol/base64_test.go @@ -4,10 +4,11 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "strings" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestBase64UnmarshalJSON(t *testing.T) { From 0a5cc27537571a75b8af4c21284fcad1c4c09624 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 11 Jun 2024 05:53:43 +1000 Subject: [PATCH 17/24] refactor: return user --- webauthn/login.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/webauthn/login.go b/webauthn/login.go index d5a01b3..5fa6aa4 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -201,21 +201,33 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe } // ValidateDiscoverableLogin is an overloaded version of ValidateLogin that allows for discoverable credentials. -func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (*Credential, error) { +// +// Note: this is just a backwards compatability layer over ValidatePasskeyLogin which returns more information. +func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (credential *Credential, err error) { + _, credential, err = webauthn.ValidatePasskeyLogin(handler, session, parsedResponse) + + return credential, err +} + +// ValidatePasskeyLogin is an overloaded version of ValidateLogin that allows for passkey credentials. +func (webauthn *WebAuthn) ValidatePasskeyLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (user User, credential *Credential, err error) { if session.UserID != nil { - return nil, protocol.ErrBadRequest.WithDetails("Session was not initiated as a client-side discoverable login") + return nil, nil, protocol.ErrBadRequest.WithDetails("Session was not initiated as a client-side discoverable login") } if parsedResponse.Response.UserHandle == nil { - return nil, protocol.ErrBadRequest.WithDetails("Client-side Discoverable Assertion was attempted with a blank User Handle") + return nil, nil, protocol.ErrBadRequest.WithDetails("Client-side Discoverable Assertion was attempted with a blank User Handle") } - user, err := handler(parsedResponse.RawID, parsedResponse.Response.UserHandle) - if err != nil { - return nil, protocol.ErrBadRequest.WithDetails(fmt.Sprintf("Failed to lookup Client-side Discoverable Credential: %s", err)) + if user, err = handler(parsedResponse.RawID, parsedResponse.Response.UserHandle); err != nil { + return nil, nil, protocol.ErrBadRequest.WithDetails(fmt.Sprintf("Failed to lookup Client-side Discoverable Credential: %s", err)) } - return webauthn.validateLogin(user, session, parsedResponse) + if credential, err = webauthn.validateLogin(user, session, parsedResponse); err != nil { + return nil, nil, err + } + + return user, credential, nil } // ValidateLogin takes a parsed response and validates it against the user credentials and session data. From 04fd67deaa2b55468f0669cdcfc59eba222d1ed0 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 9 Jul 2024 09:41:27 +1000 Subject: [PATCH 18/24] fix: use correct tag name --- metadata/decode.go | 1 + 1 file changed, 1 insertion(+) diff --git a/metadata/decode.go b/metadata/decode.go index 896e417..504bd77 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -155,6 +155,7 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) { Metadata: nil, Result: payload, DecodeHook: d.hook, + TagName: "json", }); err != nil { return nil, err } From 31c553965da4b6d8c1ed7a8584373fdac458ca2b Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 9 Jul 2024 16:04:37 +1000 Subject: [PATCH 19/24] temp --- metadata/decode.go | 59 +++++++++++++++++++++++++++ metadata/metadata.go | 2 +- metadata/metadata_test.go | 13 ++++++ metadata/providers/memory/options.go | 20 ++++++--- metadata/providers/memory/provider.go | 11 ++++- metadata/types.go | 4 ++ protocol/attestation.go | 16 ++++++++ 7 files changed, 118 insertions(+), 7 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index 504bd77..2340f8b 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -6,7 +6,9 @@ import ( "errors" "fmt" "io" + "math" "net/http" + "reflect" "strings" "time" @@ -274,3 +276,60 @@ func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err e return certificate, nil } + +// StringToMailAddressHookFunc decodes a string into a mail.Address or *mail.Address. +func hookDecodeUnsignedInt32() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data any) (value any, err error) { + switch f.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + break + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + break + case reflect.Float32, reflect.Float64: + break + default: + return data, nil + } + + if t.Kind() != reflect.Uint32 { + return data, nil + } + + var result uint64 + + switch i := data.(type) { + case int: + result = uint64(i) + case int8: + result = uint64(i) + case int16: + result = uint64(i) + case int32: + result = uint64(i) + case int64: + result = uint64(i) + case uint: + result = uint64(i) + case uint8: + result = uint64(i) + case uint16: + result = uint64(i) + case uint32: + result = uint64(i) + case uint64: + result = i + case float32: + result = uint64(i) + case float64: + result = uint64(i) + default: + return data, nil + } + + if result > math.MaxUint32 { + return 0, nil + } + + return uint32(result), nil + } +} diff --git a/metadata/metadata.go b/metadata/metadata.go index 194a6fe..794e9ec 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -653,7 +653,7 @@ type PatternAccuracyDescriptor struct { // See: https://fidoalliance.org/specs/mds/fido-metadata-statement-v3.0-ps-20210518.html#verificationmethoddescriptor-dictionary type VerificationMethodDescriptor struct { // a single USER_VERIFY constant (see [FIDORegistry]), not a bit flag combination. This value MUST be non-zero. - UserVerificationMethod string `json:"userVerification"` + UserVerificationMethod string `json:"userVerificationMethod"` // May optionally be used in the case of method USER_VERIFY_PASSCODE. CaDesc CodeAccuracyDescriptor `json:"caDesc"` diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index eeca71a..4a83497 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "math" "net/http" "testing" "time" @@ -34,6 +35,18 @@ func TestProductionMetadataTOCParsing(t *testing.T) { metadata, err = decoder.Parse(payload) require.NoError(t, err) + fmt.Println(payload.Number) + + for i, entry := range metadata.Parsed.Entries { + for _, items := range entry.MetadataStatement.UserVerificationDetails { + for _, item := range items { + if item.PaDesc.MinComplexity > math.MaxUint32 { + fmt.Printf("item %d pa complexity for %s is too large with value %d\n", i, item.UserVerificationMethod, item.PaDesc.MinComplexity) + } + } + } + } + for _, perr := range metadata.Unparsed { fmt.Println(perr.Error) } diff --git a/metadata/providers/memory/options.go b/metadata/providers/memory/options.go index 7089bed..6b8c2a1 100644 --- a/metadata/providers/memory/options.go +++ b/metadata/providers/memory/options.go @@ -20,7 +20,7 @@ func WithMetadata(mds map[uuid.UUID]*metadata.Entry) Option { // WithValidateEntry requires that the provided metadata has an entry for the given authenticator to be considered // valid. By default an AAGUID which has a zero value should fail validation if WithValidateEntryPermitZeroAAGUID is not -// provided with the value of true. +// provided with the value of true. Default is true. func WithValidateEntry(require bool) Option { return func(provider *Provider) (err error) { provider.entry = require @@ -30,7 +30,7 @@ func WithValidateEntry(require bool) Option { } // WithValidateEntryPermitZeroAAGUID is an option that permits a zero'd AAGUID from an attestation statement to -// automatically pass metadata validations. Generally helpful to use with WithValidateEntry. +// automatically pass metadata validations. Generally helpful to use with WithValidateEntry. Default is false. func WithValidateEntryPermitZeroAAGUID(permit bool) Option { return func(provider *Provider) (err error) { provider.entryPermitZero = permit @@ -40,7 +40,7 @@ func WithValidateEntryPermitZeroAAGUID(permit bool) Option { } // WithValidateTrustAnchor when set to true enables the validation of the attestation statement against the trust anchor -// from the metadata. +// from the metadata. Default is true. func WithValidateTrustAnchor(validate bool) Option { return func(provider *Provider) (err error) { provider.anchors = validate @@ -49,8 +49,8 @@ func WithValidateTrustAnchor(validate bool) Option { } } -// WithValidateStatus when set to true enables the validation of the attestation statments AAGUID against the desired -// and undesired metadata.AuthenticatorStatus lists. +// WithValidateStatus when set to true enables the validation of the attestation statements AAGUID against the desired +// and undesired metadata.AuthenticatorStatus lists. Default is true. func WithValidateStatus(validate bool) Option { return func(provider *Provider) (err error) { provider.status = validate @@ -59,6 +59,16 @@ func WithValidateStatus(validate bool) Option { } } +// WithValidateAttestationTypes when set to true enables the validation of the attestation statements type against the +// known types the authenticator can produce. Default is true. +func WithValidateAttestationTypes(validate bool) Option { + return func(provider *Provider) (err error) { + provider.status = validate + + return nil + } +} + // WithStatusUndesired provides the list of statuses which are considered undesirable for status report validation // purposes. Should be used with WithValidateStatus set to true. func WithStatusUndesired(statuses []metadata.AuthenticatorStatus) Option { diff --git a/metadata/providers/memory/provider.go b/metadata/providers/memory/provider.go index 7eef9da..27a8443 100644 --- a/metadata/providers/memory/provider.go +++ b/metadata/providers/memory/provider.go @@ -12,7 +12,11 @@ import ( // New returns a new memory Provider given a set of functional Option's. func New(opts ...Option) (provider metadata.Provider, err error) { p := &Provider{ - undesired: metadata.DefaultUndesiredAuthenticatorStatuses(), + undesired: metadata.DefaultUndesiredAuthenticatorStatuses(), + entry: true, + anchors: true, + status: true, + attestation: true, } for _, opt := range opts { @@ -39,6 +43,7 @@ type Provider struct { entryPermitZero bool anchors bool status bool + attestation bool } func (p *Provider) GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *metadata.Entry, err error) { @@ -71,6 +76,10 @@ func (p *Provider) GetValidateStatus(ctx context.Context) (validate bool) { return p.status } +func (p *Provider) GetValidateAttestationTypes(ctx context.Context) (validate bool) { + return p.attestation +} + func (p *Provider) ValidateStatusReports(ctx context.Context, reports []metadata.StatusReport) (err error) { if !p.status { return nil diff --git a/metadata/types.go b/metadata/types.go index daf5f1a..857af16 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -32,6 +32,10 @@ type Provider interface { // undesired statuses. GetValidateStatus(ctx context.Context) (validate bool) + // GetValidateAttestationTypes if true will enforce checking that the provided attestation is possible with the + // given authenticator. + GetValidateAttestationTypes(ctx context.Context) (validate bool) + // ValidateStatusReports returns nil if the provided authenticator status reports are desired. ValidateStatusReports(ctx context.Context, reports []StatusReport) (err error) } diff --git a/protocol/attestation.go b/protocol/attestation.go index fc8f0f2..adddce3 100644 --- a/protocol/attestation.go +++ b/protocol/attestation.go @@ -204,6 +204,22 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat return nil } + if mds.GetValidateAttestationTypes(ctx) { + found := false + + for _, atype := range entry.MetadataStatement.AttestationTypes { + if string(atype) == attestationType { + found = true + + break + } + } + + if !found { + return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid attestation type encountered during attestation validation. The attestation type '%s' is not known to be used by AAGUID '%s'", attestationType, aaguid.String())) + } + } + if mds.GetValidateStatus(ctx) { if err = mds.ValidateStatusReports(ctx, entry.StatusReports); err != nil { return ErrInvalidAttestation.WithDetails(fmt.Sprintf("Authenticator with invalid status encountered during attestation validation. %s", err.Error())) From b592153bf4acf543e2f58c939125b7b3156fa7bb Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 14 Jul 2024 18:22:20 +1000 Subject: [PATCH 20/24] fix: tag names --- metadata/decode.go | 4 ++-- metadata/metadata.go | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index 2340f8b..34335ca 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -96,8 +96,6 @@ func (d *Decoder) Decode(r io.ReadCloser) (payload *PayloadJSON, err error) { // DecodeBytes handles decoding raw bytes. If you have a read closer it's suggested to use Decode. func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) { - payload = &PayloadJSON{} - var token *jwt.Token if token, err = d.parser.Parse(string(bytes), func(token *jwt.Token) (any, error) { @@ -153,6 +151,8 @@ func (d *Decoder) DecodeBytes(bytes []byte) (payload *PayloadJSON, err error) { var decoder *mapstructure.Decoder + payload = &PayloadJSON{} + if decoder, err = mapstructure.NewDecoder(&mapstructure.DecoderConfig{ Metadata: nil, Result: payload, diff --git a/metadata/metadata.go b/metadata/metadata.go index 794e9ec..0672b42 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -253,6 +253,9 @@ type Statement struct { // The FIDO protocol family. The values "uaf", "u2f", and "fido2" are supported. ProtocolFamily string + // he Metadata Schema version. + Schema uint16 + // The FIDO unified protocol version(s) (related to the specific protocol family) supported by this authenticator. Upv []Version @@ -342,6 +345,7 @@ type StatementJSON struct { AlternativeDescriptions map[string]string `json:"alternativeDescriptions"` AuthenticatorVersion uint32 `json:"authenticatorVersion"` ProtocolFamily string `json:"protocolFamily"` + Schema uint16 `json:"schema"` Upv []Version `json:"upv"` AuthenticationAlgorithms []AuthenticationAlgorithm `json:"authenticationAlgorithms"` PublicKeyAlgAndEncodings []PublicKeyAlgAndEncoding `json:"publicKeyAlgAndEncodings"` @@ -405,6 +409,7 @@ func (j StatementJSON) Parse() (statement Statement, err error) { AlternativeDescriptions: j.AlternativeDescriptions, AuthenticatorVersion: j.AuthenticatorVersion, ProtocolFamily: j.ProtocolFamily, + Schema: j.Schema, Upv: j.Upv, AuthenticationAlgorithms: j.AuthenticationAlgorithms, PublicKeyAlgAndEncodings: j.PublicKeyAlgAndEncodings, @@ -834,7 +839,7 @@ type AuthenticatorGetInfoJSON struct { MaxMsgSize uint `json:"maxMsgSize"` PivUvAuthProtocols []uint `json:"pinUvAuthProtocols"` MaxCredentialCountInList uint `json:"maxCredentialCountInList"` - MaxCredentialIdLength uint `json:"maxCredentialLength"` + MaxCredentialIdLength uint `json:"maxCredentialIdLength"` Transports []string `json:"transports"` Algorithms []PublicKeyCredentialParameters `json:"algorithms"` MaxSerializedLargeBlobArray uint `json:"maxSerializedLargeBlobArray"` From e6215484fc5c107e128399e4fbda38f254fc6128 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Sun, 14 Jul 2024 19:35:26 +1000 Subject: [PATCH 21/24] fix: prints --- metadata/metadata_test.go | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go index 4a83497..b720fdf 100644 --- a/metadata/metadata_test.go +++ b/metadata/metadata_test.go @@ -4,9 +4,7 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "io" - "math" "net/http" "testing" "time" @@ -34,22 +32,7 @@ func TestProductionMetadataTOCParsing(t *testing.T) { metadata, err = decoder.Parse(payload) require.NoError(t, err) - - fmt.Println(payload.Number) - - for i, entry := range metadata.Parsed.Entries { - for _, items := range entry.MetadataStatement.UserVerificationDetails { - for _, item := range items { - if item.PaDesc.MinComplexity > math.MaxUint32 { - fmt.Printf("item %d pa complexity for %s is too large with value %d\n", i, item.UserVerificationMethod, item.PaDesc.MinComplexity) - } - } - } - } - - for _, perr := range metadata.Unparsed { - fmt.Println(perr.Error) - } + require.NotNil(t, metadata) } func TestConformanceMetadataTOCParsing(t *testing.T) { From 23c3bce64679bcfca531cfb7686ad9d12d7924af Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 17 Jul 2024 22:01:46 +1000 Subject: [PATCH 22/24] Apply suggestions from code review Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- metadata/decode.go | 2 +- webauthn/login.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index 34335ca..c83af1b 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -27,7 +27,7 @@ func NewDecoder(opts ...DecoderOption) (decoder *Decoder, err error) { for _, opt := range opts { if err = opt(decoder); err != nil { - return nil, err + return nil, fmt.Errorf("failed to apply decoder option: %w", err) } } diff --git a/webauthn/login.go b/webauthn/login.go index 5fa6aa4..89ff5f8 100644 --- a/webauthn/login.go +++ b/webauthn/login.go @@ -202,7 +202,7 @@ func (webauthn *WebAuthn) ValidateLogin(user User, session SessionData, parsedRe // ValidateDiscoverableLogin is an overloaded version of ValidateLogin that allows for discoverable credentials. // -// Note: this is just a backwards compatability layer over ValidatePasskeyLogin which returns more information. +// Note: this is just a backwards compatibility layer over ValidatePasskeyLogin which returns more information. func (webauthn *WebAuthn) ValidateDiscoverableLogin(handler DiscoverableUserHandler, session SessionData, parsedResponse *protocol.ParsedCredentialAssertionData) (credential *Credential, err error) { _, credential, err = webauthn.ValidatePasskeyLogin(handler, session, parsedResponse) From e1ca14477dae6d9b29f6fcabc825fbb7be606710 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 17 Jul 2024 22:03:04 +1000 Subject: [PATCH 23/24] fix: suggestions --- metadata/decode.go | 59 -------------------------------------------- metadata/metadata.go | 4 +-- 2 files changed, 1 insertion(+), 62 deletions(-) diff --git a/metadata/decode.go b/metadata/decode.go index c83af1b..f3c3de7 100644 --- a/metadata/decode.go +++ b/metadata/decode.go @@ -6,9 +6,7 @@ import ( "errors" "fmt" "io" - "math" "net/http" - "reflect" "strings" "time" @@ -276,60 +274,3 @@ func mdsParseX509Certificate(value string) (certificate *x509.Certificate, err e return certificate, nil } - -// StringToMailAddressHookFunc decodes a string into a mail.Address or *mail.Address. -func hookDecodeUnsignedInt32() mapstructure.DecodeHookFuncType { - return func(f reflect.Type, t reflect.Type, data any) (value any, err error) { - switch f.Kind() { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - break - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - break - case reflect.Float32, reflect.Float64: - break - default: - return data, nil - } - - if t.Kind() != reflect.Uint32 { - return data, nil - } - - var result uint64 - - switch i := data.(type) { - case int: - result = uint64(i) - case int8: - result = uint64(i) - case int16: - result = uint64(i) - case int32: - result = uint64(i) - case int64: - result = uint64(i) - case uint: - result = uint64(i) - case uint8: - result = uint64(i) - case uint16: - result = uint64(i) - case uint32: - result = uint64(i) - case uint64: - result = i - case float32: - result = uint64(i) - case float64: - result = uint64(i) - default: - return data, nil - } - - if result > math.MaxUint32 { - return 0, nil - } - - return uint32(result), nil - } -} diff --git a/metadata/metadata.go b/metadata/metadata.go index 0672b42..0a43e68 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -908,9 +908,7 @@ type MDSGetEndpointsResponse struct { func DefaultUndesiredAuthenticatorStatuses() []AuthenticatorStatus { undesired := make([]AuthenticatorStatus, len(defaultUndesiredAuthenticatorStatus)) - for i := range defaultUndesiredAuthenticatorStatus { - undesired[i] = defaultUndesiredAuthenticatorStatus[i] - } + copy(undesired, defaultUndesiredAuthenticatorStatus[:]) return undesired } From e056df014360dbafefddb094a63b86e24aeb5328 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 17 Jul 2024 22:35:27 +1000 Subject: [PATCH 24/24] docs: add doc string --- metadata/types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/metadata/types.go b/metadata/types.go index 857af16..1562b63 100644 --- a/metadata/types.go +++ b/metadata/types.go @@ -11,6 +11,7 @@ import ( "github.com/go-webauthn/webauthn/protocol/webauthncose" ) +// The Provider is an interface which describes the elements required to satisfy validation of metadata. type Provider interface { // GetEntry returns a MDS3 payload entry given a AAGUID. This GetEntry(ctx context.Context, aaguid uuid.UUID) (entry *Entry, err error)