Repository: go-webauthn/webauthn (go)

Issue:
Attestation with full attestation from authenticator that does not support full attestation
### Version

0.8.2

### Description

Doing an enrollment with a Windows Hello Authenticator device with "direct" attestation throws this error that happens at attestation.go:187. 

This happens because the MetatadataStatement for Windows Hello Authenticator (aaguid 08987058-cadc-4b81-b6e1-30de50dcbe96) doesn't have "basic_full" in the attestationTypes array. 

 "attestationTypes": [
          "attca"
 ]

Is this a correct behavior? 

### Reproduction

Enroll a device with:
AttestationType "direct"
AuthenticatorType "platform"
Authenticator: "Windows Hello Authenticator"

### Expectations

The device should be enrolled succesfully.

### Documentation

_No response_

Pull request description:
Treat attestation type attca as a full attestation like basic_full
- For purposes of determining if an authenticator has produced a full attestation when the MDS entry for the authenticator indicates  cannot produce a full attestation, treat `attca` and `basic_full` both as full attestation types
- Add test data with an authenticator aaguid that only has `attca` type
- Make production metadata available for use in tests

Resolves #149

Patch:
diff --git a/metadata/metadata.go b/metadata/metadata.go
index 7ce5834..f0701ad 100644
--- a/metadata/metadata.go
+++ b/metadata/metadata.go
@@ -4,8 +4,10 @@ import (
 	"crypto/x509"
 	"encoding/base64"
 	"errors"
+	"io"
 	"net/http"
 	"reflect"
+	"time"
 
 	"github.com/golang-jwt/jwt/v4"
 	"github.com/google/uuid"
@@ -24,6 +26,8 @@ type PublicKeyCredentialParameters struct {
 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
@@ -693,3 +697,33 @@ var (
 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
+}


Test patch:
diff --git a/metadata/metadata_test.go b/metadata/metadata_test.go
index 03fc800..acb3381 100644
--- a/metadata/metadata_test.go
+++ b/metadata/metadata_test.go
@@ -93,18 +93,9 @@ func getTestMetadata(s string, c http.Client) (MetadataStatement, error) {
 }
 
 func TestProductionMetadataTOCParsing(t *testing.T) {
-	httpClient := &http.Client{
-		Timeout: time.Second * 30,
-	}
-
-	bytes, err := downloadBytes("https://mds.fidoalliance.org/", *httpClient)
-	if err != nil {
+	if err := PopulateMetadata(ProductionMDSURL); err != nil {
 		t.Fatal(err)
 	}
-
-	if _, err = unmarshalMDSBLOB(bytes, *httpClient); err != nil {
-		t.Fail()
-	}
 }
 
 func TestConformanceMetadataTOCParsing(t *testing.T) {
diff --git a/protocol/attestation.go b/protocol/attestation.go
index 80e6633..54716de 100644
--- a/protocol/attestation.go
+++ b/protocol/attestation.go
@@ -178,7 +178,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client
 				var hasBasicFull = false
 
 				for _, a := range meta.MetadataStatement.AttestationTypes {
-					if a == metadata.BasicFull {
+					if a == metadata.BasicFull || a == metadata.AttCA {
 						hasBasicFull = true
 					}
 				}
diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go
index 1c49ef8..f714429 100644
--- a/protocol/attestation_test.go
+++ b/protocol/attestation_test.go
@@ -5,9 +5,15 @@ import (
 	"encoding/json"
 	"fmt"
 	"testing"
+
+	"github.com/go-webauthn/webauthn/metadata"
 )
 
 func TestAttestationVerify(t *testing.T) {
+	if err := metadata.PopulateMetadata(metadata.ProductionMDSURL); err != nil {
+		t.Fatal(err)
+	}
+
 	for i := range testAttestationOptions {
 		t.Run(fmt.Sprintf("Running test %d", i), func(t *testing.T) {
 			options := CredentialCreation{}
@@ -68,6 +74,7 @@ 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])
 
@@ -159,6 +166,28 @@ var testAttestationOptions = []string{
 		  "attestation": "none"
 		}
 	  }`,
+	`{
+		"publicKey": {
+			"rp": {
+				"name": "https://gramthanos.github.io",
+				"id": "gramthanos.github.io"
+			},
+			"user": {
+				"name": "john.smith@email.com",
+				"displayName": "J. Smith",
+				"id": "am9obi5zbWl0aEBlbWFpbC5jb20="
+			},
+			"challenge": "Dw4NDAsKCQgHBgUEAwIBAA==",
+			"pubKeyCredParams": [
+				{"type": "public-key", "alg": -7},
+				{"type": "public-key", "alg": -37},
+				{"type": "public-key", "alg": -257},
+				{"type": "public-key", "alg": -8}
+			],
+			"timeout": 120000,
+			"attestation": "direct"
+		}
+	}`,
 }
 
 var testAttestationResponses = []string{
@@ -191,5 +220,13 @@ var testAttestationResponses = []string{
 			"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJzVnQ0U2NjZU16cUZTbmZBcThoZ0x6Ymx2bzNmYTRfYUZWRWNJRVNISUowIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
 		},
 		"type": "public-key"
+	}`, `{
+		"type": "public-key",
+		"id": "GramThanos8pyTMpdk0qJLv3eLhUP3EXIXjD-uyqD0gab1pdvGy1ig77ZLl_ZU_vnd2296FoIZ67pZqTChpSJPq_oqUhjmr5Osv_LLiY7YGsAafMUdIb_LKOdwc6sfXyy_Ygl3_w-vl3tU9EPGyzgtI7hTBeMXnSIaOV6CUUf6d9op4JyxEDJr-roWxRMJPfnVAMLvv4lF_Cpd6Of0o75nDcCtEsTiynINihIwee1gmg0BAVKh3seWoNqXMpiXgPWc9Jt8ibjN9O-bsag3tELVs9uOoe-NZEmwbph0jJh_Y6e2H5Nwkp7WghST0P6krTL_sUlbpmDolhfFut0YljLrOrz_llW-WHySwvaAG2vzgvxA",
+		"rawId": "GramThanos8pyTMpdk0qJLv3eLhUP3EXIXjD-uyqD0gab1pdvGy1ig77ZLl_ZU_vnd2296FoIZ67pZqTChpSJPq_oqUhjmr5Osv_LLiY7YGsAafMUdIb_LKOdwc6sfXyy_Ygl3_w-vl3tU9EPGyzgtI7hTBeMXnSIaOV6CUUf6d9op4JyxEDJr-roWxRMJPfnVAMLvv4lF_Cpd6Of0o75nDcCtEsTiynINihIwee1gmg0BAVKh3seWoNqXMpiXgPWc9Jt8ibjN9O-bsag3tELVs9uOoe-NZEmwbph0jJh_Y6e2H5Nwkp7WghST0P6krTL_sUlbpmDolhfFut0YljLrOrz_llW-WHySwvaAG2vzgvxA",
+		"response": {
+			"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiRHc0TkRBc0tDUWdIQmdVRUF3SUJBQSIsIm9yaWdpbiI6Imh0dHBzOi8vZ3JhbXRoYW5vcy5naXRodWIuaW8iLCJjcm9zc09yaWdpbiI6ZmFsc2UsInZpcnR1YWxfYXV0aGVudGljYXRvciI6IkdyYW1UaGFub3MgJiBVbml2ZXJzaXR5IG9mIFBpcmFldXMifQ",
+			"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEYwRAIgaTjQj-hC9GH1fCbOT_8m4wdVJBZMG0252iBEwIGKWkUCIApZyPGh_ihn57GRKN-qTVCwgBqe4V40LL-r9_Y2pRXiY3g1Y4FZAgUwggIBMIIBpqADAgECAgVixtGpsjAKBggqhkjOPQQDAjBQMQswCQYDVQQGEwJHUjESMBAGA1UECgwJVU5JUEkgU1NMMS0wKwYDVQQDEyRVTklQSSBGSURPMiBWaXJ0dWFsIEF1dGhlbnRpY2F0b3IgQ0EwIhgPMjAyMDEyMzEyMjAwMDBaGA8yMTIwMTIzMTIyMDAwMFowcTELMAkGA1UEBhMCR1IxEjAQBgNVBAoMCVVOSVBJIFNTTDEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEqMCgGA1UEAwwhVU5JUEkgRklETzIgVmlydHVhbCBBdXRoZW50aWNhdG9yMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE_l8G-E0tTiXogmgXZZ0nRUMc7NO5-sowWP0lZhX8GZbU_n2TPO1J-39UbRABHUK_2J-ZbzcDAu2oy_nazsz4CqNIMEYwIQYLKwYBBAGC5RwBAQQEEgQQCJhwWMrcS4G24TDeUNy-ljATBgsrBgEEAYLlHAIBAQQEAwIFIDAMBgNVHRMBAf8EAjAAMAoGCCqGSM49BAMCA0kAMEYCIQDsyXh97GlMAcRq8khd4U-26d1E92a0lupZUGNBlki_MQIhAJFqO_qmBakyeD1esP4v3gIWsYKmHpiwJ64UKlid5NobaGF1dGhEYXRhWQGWou-FTChrR7AO-C0KXtsaxN1QIX4DOq_aCmYeKeUXnlZFAAAAAQiYcFjK3EuBtuEw3lDcvpYBEhq2pk4Wp6LPKckzKXZNKiS793i4VD9xFyF4w_rsqg9IGm9aXbxstYoO-2S5f2VP753dtvehaCGeu6WakwoaUiT6v6KlIY5q-TrL_yy4mO2BrAGnzFHSG_yyjncHOrH18sv2IJd_8Pr5d7VPRDxss4LSO4UwXjF50iGjleglFH-nfaKeCcsRAya_q6FsUTCT351QDC77-JRfwqXejn9KO-Zw3ArRLE4spyDYoSMHntYJoNAQFSod7HlqDalzKYl4D1nPSbfIm4zfTvm7GoN7RC1bPbjqHvjWRJsG6YdIyYf2Onth-TcJKe1oIUk9D-pK0y_7FJW6Zg6JYXxbrdGJYy6zq8_5ZVvlh8ksL2gBtr84L8SlAQIDJiABIVgg_l8G-E0tTiXogmgXZZ0nRUMc7NO5-sowWP0lZhX8GZYiWCDU_n2TPO1J-39UbRABHUK_2J-ZbzcDAu2oy_nazsz4Cg"
+		}
 	}`,
 }