Repository: go-webauthn/webauthn
Pull request #113 (2023-02-13 23:52:56)

fix(protocol): creation invalid transports path
The transports JSON path as far as the Webauthn level 3 spec is concerned is response.transports i.e. within the response object with response.clientDataJSON and response.attestationObject. This fixes this location and allows for backwards temporary compatibility for anyone implementing the previous method. See the typedef https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson for more information.


Patch:
diff --git a/protocol/assertion.go b/protocol/assertion.go
index 45a4578..38e4c6d 100644
--- a/protocol/assertion.go
+++ b/protocol/assertion.go
@@ -51,6 +51,7 @@ func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAs
 	if response == nil || response.Body == nil {
 		return nil, ErrBadRequest.WithDetails("No response given")
 	}
+
 	return ParseCredentialRequestResponseBody(response.Body)
 }
 
@@ -58,10 +59,10 @@ func ParseCredentialRequestResponse(response *http.Request) (*ParsedCredentialAs
 // or makes the assertion verification steps easier to complete. This takes an io.Reader that contains
 // the assertion response data in a raw, mostly base64 encoded format, and parses the data into
 // manageable structures
-func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssertionData, error) {
+func ParseCredentialRequestResponseBody(body io.Reader) (par *ParsedCredentialAssertionData, err error) {
 	var car CredentialAssertionResponse
-	err := json.NewDecoder(body).Decode(&car)
-	if err != nil {
+
+	if err = json.NewDecoder(body).Decode(&car); err != nil {
 		return nil, ErrBadRequest.WithDetails("Parse error for Assertion")
 	}
 
@@ -69,32 +70,36 @@ func ParseCredentialRequestResponseBody(body io.Reader) (*ParsedCredentialAssert
 		return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID missing")
 	}
 
-	_, err = base64.RawURLEncoding.DecodeString(car.ID)
-	if err != nil {
+	if _, err = base64.RawURLEncoding.DecodeString(car.ID); err != nil {
 		return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with ID not base64url encoded")
 	}
+
 	if car.Type != "public-key" {
 		return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
 	}
-	var par ParsedCredentialAssertionData
-	par.ID, par.RawID, par.Type, par.ClientExtensionResults = car.ID, car.RawID, car.Type, car.ClientExtensionResults
-	par.Raw = car
 
-	par.Response.Signature = car.AssertionResponse.Signature
-	par.Response.UserHandle = car.AssertionResponse.UserHandle
+	par = &ParsedCredentialAssertionData{
+		ParsedPublicKeyCredential{
+			ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults,
+		},
+		ParsedAssertionResponse{
+			Signature:  car.AssertionResponse.Signature,
+			UserHandle: car.AssertionResponse.UserHandle,
+		},
+		car,
+	}
 
 	// Step 5. Let JSONtext be the result of running UTF-8 decode on the value of cData.
 	// We don't call it cData but this is Step 5 in the spec.
-	err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData)
-	if err != nil {
+	if err = json.Unmarshal(car.AssertionResponse.ClientDataJSON, &par.Response.CollectedClientData); err != nil {
 		return nil, err
 	}
 
-	err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData)
-	if err != nil {
+	if err = par.Response.AuthenticatorData.Unmarshal(car.AssertionResponse.AuthenticatorData); err != nil {
 		return nil, ErrParsingData.WithDetails("Error unmarshalling auth data")
 	}
-	return &par, nil
+
+	return par, nil
 }
 
 // Follow the remaining steps outlined in §7.2 Verifying an authentication assertion
diff --git a/protocol/credential.go b/protocol/credential.go
index 117c20d..8f03b71 100644
--- a/protocol/credential.go
+++ b/protocol/credential.go
@@ -50,9 +50,8 @@ type CredentialCreationResponse struct {
 
 type ParsedCredentialCreationData struct {
 	ParsedPublicKeyCredential
-	Response   ParsedAttestationResponse
-	Transports []AuthenticatorTransport
-	Raw        CredentialCreationResponse
+	Response ParsedAttestationResponse
+	Raw      CredentialCreationResponse
 }
 
 func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialCreationData, error) {
@@ -62,10 +61,10 @@ func ParseCredentialCreationResponse(response *http.Request) (*ParsedCredentialC
 	return ParseCredentialCreationResponseBody(response.Body)
 }
 
-func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreationData, error) {
+func ParseCredentialCreationResponseBody(body io.Reader) (pcc *ParsedCredentialCreationData, err error) {
 	var ccr CredentialCreationResponse
-	err := json.NewDecoder(body).Decode(&ccr)
-	if err != nil {
+
+	if err = json.NewDecoder(body).Decode(&ccr); err != nil {
 		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo(err.Error())
 	}
 
@@ -86,26 +85,32 @@ func ParseCredentialCreationResponseBody(body io.Reader) (*ParsedCredentialCreat
 		return nil, ErrBadRequest.WithDetails("Parse error for Registration").WithInfo("Type not public-key")
 	}
 
-	var pcc ParsedCredentialCreationData
-	pcc.ID, pcc.RawID, pcc.Type, pcc.ClientExtensionResults = ccr.ID, ccr.RawID, ccr.Type, ccr.ClientExtensionResults
-	pcc.Raw = ccr
-
-	for _, t := range ccr.Transports {
-		pcc.Transports = append(pcc.Transports, AuthenticatorTransport(t))
-	}
-
-	parsedAttestationResponse, err := ccr.AttestationResponse.Parse()
+	response, err := ccr.AttestationResponse.Parse()
 	if err != nil {
 		return nil, ErrParsingData.WithDetails("Error parsing attestation response")
 	}
 
-	pcc.Response = *parsedAttestationResponse
+	// TODO: Remove this as it's a backwards compatability layer.
+	if len(response.Transports) == 0 && len(ccr.Transports) != 0 {
+		for _, t := range ccr.Transports {
+			response.Transports = append(response.Transports, AuthenticatorTransport(t))
+		}
+	}
 
-	return &pcc, nil
+	return &ParsedCredentialCreationData{
+		ParsedPublicKeyCredential{
+			ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults,
+		},
+		*response,
+		ccr,
+	}, nil
 }
 
-// Verifies the Client and Attestation data as laid out by §7.1. Registering a new credential
-// https://www.w3.org/TR/webauthn/#registering-a-new-credential
+// Verify the Client and Attestation data.
+//
+// From: §7.1. Registering a New Credential
+//
+// See: https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential
 func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, relyingPartyOrigins []string) error {
 
 	// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
@@ -122,7 +127,7 @@ func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUs
 	// attestation statement attStmt. is handled while
 
 	// We do the above step while parsing and decoding the CredentialCreationResponse
-	// Handle steps 9 through 14 - This verifies the attestaion object and
+	// Handle steps 9 through 14 - This verifies the attestation object and
 	verifyError = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash[:], verifyUser)
 	if verifyError != nil {
 		return verifyError
@@ -131,7 +136,7 @@ func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUs
 	// Step 15. If validation is successful, obtain a list of acceptable trust anchors (attestation root
 	// certificates or ECDAA-Issuer public keys) for that attestation type and attestation statement
 	// format fmt, from a trusted source or from policy. For example, the FIDO Metadata Service provides
-	// one way to obtain such information, using the aaguid in the attestedCredentialData in authData.
+	// one way to obtain such information, using the AAGUID in the attestedCredentialData in authData.
 	// [https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-metadata-service-v2.0-id-20180227.html]
 
 	// TODO: There are no valid AAGUIDs yet or trust sources supported. We could implement policy for the RP in
diff --git a/protocol/errors.go b/protocol/errors.go
index 681bd8b..bfd1ff3 100644
--- a/protocol/errors.go
+++ b/protocol/errors.go
@@ -31,7 +31,7 @@ var (
 		Details: "Error validating the authenticator response",
 	}
 	ErrAttestation = &Error{
-		Type:    "attesation_error",
+		Type:    "attestation_error",
 		Details: "Error validating the attestation data provided",
 	}
 	ErrInvalidAttestation = &Error{
diff --git a/webauthn/credential.go b/webauthn/credential.go
index 62bf08a..4b08dc9 100644
--- a/webauthn/credential.go
+++ b/webauthn/credential.go
@@ -37,7 +37,7 @@ func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, e
 		ID:              c.Response.AttestationObject.AuthData.AttData.CredentialID,
 		PublicKey:       c.Response.AttestationObject.AuthData.AttData.CredentialPublicKey,
 		AttestationType: c.Response.AttestationObject.Format,
-		Transport:       c.Transports,
+		Transport:       c.Response.Transports,
 		Authenticator: Authenticator{
 			AAGUID:    c.Response.AttestationObject.AuthData.AttData.AAGUID,
 			SignCount: c.Response.AttestationObject.AuthData.Counter,


Test patch:
diff --git a/protocol/assertion_test.go b/protocol/assertion_test.go
index 2ccda77..234c0bb 100644
--- a/protocol/assertion_test.go
+++ b/protocol/assertion_test.go
@@ -3,18 +3,16 @@ package protocol
 import (
 	"bytes"
 	"encoding/base64"
-	"io/ioutil"
-	"net/http"
-	"reflect"
+	"io"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
 	"github.com/go-webauthn/webauthn/protocol/webauthncbor"
 )
 
 func TestParseCredentialRequestResponse(t *testing.T) {
-	reqBody := ioutil.NopCloser(bytes.NewReader([]byte(testAssertionResponses["success"])))
-	httpReq := &http.Request{Body: reqBody}
-
 	byteID, _ := base64.RawURLEncoding.DecodeString("AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng")
 	byteAAGUID, _ := base64.RawURLEncoding.DecodeString("rc4AAjW8xgpkiwsl8fBVAw")
 	byteRPIDHash, _ := base64.RawURLEncoding.DecodeString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA")
@@ -25,26 +23,29 @@ func TestParseCredentialRequestResponse(t *testing.T) {
 	byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJFNFBUY0lIX0hmWDFwQzZTaWdrMVNDOU5BbGdlenROMDQzOXZpOHpfYzlrIiwibmV3X2tleXNfbWF5X2JlX2FkZGVkX2hlcmUiOiJkbyBub3QgY29tcGFyZSBjbGllbnREYXRhSlNPTiBhZ2FpbnN0IGEgdGVtcGxhdGUuIFNlZSBodHRwczovL2dvby5nbC95YWJQZXgiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwidHlwZSI6IndlYmF1dGhuLmdldCJ9")
 
 	type args struct {
-		response *http.Request
+		responseName string
 	}
-	tests := []struct {
-		name    string
-		args    args
-		want    *ParsedCredentialAssertionData
-		wantErr bool
+	testCases := []struct {
+		name      string
+		args      args
+		expected  *ParsedCredentialAssertionData
+		errString string
 	}{
 		{
-			name: "Successfully Parse Credential Assertion",
+			name: "ShouldParseCredentialAssertion",
 			args: args{
-				httpReq,
+				"success",
 			},
-			want: &ParsedCredentialAssertionData{
+			expected: &ParsedCredentialAssertionData{
 				ParsedPublicKeyCredential: ParsedPublicKeyCredential{
 					ParsedCredential: ParsedCredential{
 						ID:   "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
 						Type: "public-key",
 					},
 					RawID: byteID,
+					ClientExtensionResults: map[string]interface{}{
+						"appID": "example.com",
+					},
 				},
 				Response: ParsedAssertionResponse{
 					CollectedClientData: CollectedClientData{
@@ -73,6 +74,9 @@ func TestParseCredentialRequestResponse(t *testing.T) {
 							ID:   "AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
 						},
 						RawID: byteID,
+						ClientExtensionResults: map[string]interface{}{
+							"appID": "example.com",
+						},
 					},
 					AssertionResponse: AuthenticatorAssertionResponse{
 						AuthenticatorResponse: AuthenticatorResponse{
@@ -84,62 +88,49 @@ func TestParseCredentialRequestResponse(t *testing.T) {
 					},
 				},
 			},
-			wantErr: false,
+			errString: "",
 		},
 	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := ParseCredentialRequestResponse(tt.args.response)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("ParseCredentialRequestResponse() error = %v, wantErr %v", err, tt.wantErr)
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			body := io.NopCloser(bytes.NewReader([]byte(testAssertionResponses[tc.args.responseName])))
+
+			actual, err := ParseCredentialRequestResponseBody(body)
+			if tc.errString != "" {
+				assert.EqualError(t, err, tc.errString)
+
 				return
 			}
-			if !reflect.DeepEqual(got.ClientExtensionResults, tt.want.ClientExtensionResults) {
-				t.Errorf("ClientExtensionResults = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.ID, tt.want.ID) {
-				t.Errorf("ID = %v \n want: %v", got.ID, tt.want.ID)
-			}
-			if !reflect.DeepEqual(got.ParsedCredential, tt.want.ParsedCredential) {
-				t.Errorf("ParsedCredential = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.ParsedPublicKeyCredential, tt.want.ParsedPublicKeyCredential) {
-				t.Errorf("ParsedPublicKeyCredential = %v \n want: %v", got.ParsedPublicKeyCredential.ClientExtensionResults, tt.want.ParsedPublicKeyCredential.ClientExtensionResults)
-			}
-			if !reflect.DeepEqual(got.Raw, tt.want.Raw) {
-				t.Errorf("Raw = %+v \n want: %+v", got.Raw, tt.want.Raw)
-			}
-			if !reflect.DeepEqual(got.RawID, tt.want.RawID) {
-				t.Errorf("RawID = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.Response, tt.want.Response) {
-				var pkInterfaceMismatch bool
-				if !reflect.DeepEqual(got.Response.CollectedClientData, tt.want.Response.CollectedClientData) {
-					t.Errorf("Collected Client Data = %v \n want: %v", got.Response.CollectedClientData, tt.want.Response.CollectedClientData)
-				}
-				if !reflect.DeepEqual(got.Response.Signature, tt.want.Response.Signature) {
-					t.Errorf("Signature = %v \n want: %v", got.Response.Signature, tt.want.Response.Signature)
-				}
-				if !reflect.DeepEqual(got.Response.AuthenticatorData.AttData.CredentialPublicKey, tt.want.Response.AuthenticatorData.AttData.CredentialPublicKey) {
-					// Unmarshall CredentialPublicKey
-					var pkWant interface{}
-					keyBytesWant := tt.want.Response.AuthenticatorData.AttData.CredentialPublicKey
-					webauthncbor.Unmarshal(keyBytesWant, &pkWant)
-					var pkGot interface{}
-					keyBytesGot := got.Response.AuthenticatorData.AttData.CredentialPublicKey
-					webauthncbor.Unmarshal(keyBytesGot, &pkGot)
-					if !reflect.DeepEqual(pkGot, pkWant) {
-						t.Errorf("Response = %+v \n want: %+v", pkGot, pkWant)
-					} else {
-						pkInterfaceMismatch = true
-					}
-				}
-				if pkInterfaceMismatch {
-					return
-				} else {
-					t.Errorf("Response = %+v \n want: %+v", got.Response, tt.want.Response)
-				}
-			}
+
+			require.NoError(t, err)
+
+			assert.Equal(t, tc.expected.ClientExtensionResults, actual.ClientExtensionResults)
+			assert.Equal(t, tc.expected.ID, actual.ID)
+			assert.Equal(t, tc.expected.ParsedCredential, actual.ParsedCredential)
+			assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
+			assert.Equal(t, tc.expected.Raw, actual.Raw)
+			assert.Equal(t, tc.expected.RawID, actual.RawID)
+
+			assert.Equal(t, tc.expected.Response.CollectedClientData, actual.Response.CollectedClientData)
+
+			var (
+				pkExpected, pkActual interface{}
+			)
+
+			keyBytesExpected := tc.expected.Response.AuthenticatorData.AttData.CredentialPublicKey
+
+			assert.NoError(t, webauthncbor.Unmarshal(keyBytesExpected, &pkExpected))
+
+			keyBytesActual := actual.Response.AuthenticatorData.AttData.CredentialPublicKey
+
+			assert.NoError(t, webauthncbor.Unmarshal(keyBytesActual, &pkActual))
+
+			assert.Equal(t, pkExpected, pkActual)
+			assert.NotEqual(t, nil, pkExpected)
+			assert.NotEqual(t, nil, pkActual)
 		})
 	}
 }
@@ -180,24 +171,12 @@ func TestParsedCredentialAssertionData_Verify(t *testing.T) {
 	}
 }
 
-var testAssertionOptions = map[string]string{
-	// None Attestation - MacOS TouchID
-	`success`: `{
-		"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
-		"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
-		"type":"public-key",
-		"response":{
-			"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVi7dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiFa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
-			"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJmeWV1dUdQOXp1ZWoyRkdqZXZpNzlienFNS1d4aTRQWUlhXzV3ajI2MVcwIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"}
-		}
-	`,
-}
-
 var testAssertionResponses = map[string]string{
 	// None Attestation - MacOS TouchID
 	`success`: `{
 		"id":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
 		"rawId":"AI7D5q2P0LS-Fal9ZT7CHM2N5BLbUunF92T8b6iYC199bO2kagSuU05-5dZGqb1SP0A0lyTWng",
+		"clientExtensionResults":{"appID":"example.com"},
 		"type":"public-key",
 		"response":{
 			"authenticatorData":"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFXJJiGa3OAAI1vMYKZIsLJfHwVQMANwCOw-atj9C0vhWpfWU-whzNjeQS21Lpxfdk_G-omAtffWztpGoErlNOfuXWRqm9Uj9ANJck1p6lAQIDJiABIVggKAhfsdHcBIc0KPgAcRyAIK_-Vi-nCXHkRHPNaCMBZ-4iWCBxB8fGYQSBONi9uvq0gv95dGWlhJrBwCsj_a4LJQKVHQ",
diff --git a/protocol/attestation.go b/protocol/attestation.go
index 4da9c28..86a3ca3 100644
--- a/protocol/attestation.go
+++ b/protocol/attestation.go
@@ -6,28 +6,23 @@ import (
 	"encoding/json"
 	"fmt"
 
+	"github.com/google/uuid"
+
 	"github.com/go-webauthn/webauthn/metadata"
 	"github.com/go-webauthn/webauthn/protocol/webauthncbor"
-	"github.com/google/uuid"
 )
 
-// From §5.2.1 (https://www.w3.org/TR/webauthn/#authenticatorattestationresponse)
-// "The authenticator's response to a client’s request for the creation
-// of a new public key credential. It contains information about the new credential
-// that can be used to identify it for later use, and metadata that can be used by
-// the WebAuthn Relying Party to assess the characteristics of the credential
-// during registration."
-
-// The initial unpacked 'response' object received by the relying party. This
-// contains the clientDataJSON object, which will be marshalled into
-// CollectedClientData, and the 'attestationObject', which contains
-// information about the authenticator, and the newly minted
-// public key credential. The information in both objects are used
-// to verify the authenticity of the ceremony and new credential
+// AuthenticatorAttestationResponse is the initial unpacked 'response' object received by the relying party. This
+// contains the clientDataJSON object, which will be marshalled into CollectedClientData, and the 'attestationObject',
+// which contains information about the authenticator, and the newly minted public key credential. The information in
+// both objects are used to verify the authenticity of the ceremony and new credential.
+//
+// See: https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson
 type AuthenticatorAttestationResponse struct {
 	// The byte slice of clientDataJSON, which becomes CollectedClientData
 	AuthenticatorResponse
-	// The byte slice version of AttestationObject
+
+	// AttestationObject is the byte slice version of attestationObject.
 	// This attribute contains an attestation object, which is opaque to, and
 	// cryptographically protected against tampering by, the client. The
 	// attestation object contains both authenticator data and an attestation
@@ -38,23 +33,31 @@ type AuthenticatorAttestationResponse struct {
 	// requires to validate the attestation statement, as well as to decode and
 	// validate the authenticator data along with the JSON-serialized client data.
 	AttestationObject URLEncodedBase64 `json:"attestationObject"`
+
+	Transports []string `json:"transports,omitempty"`
 }
 
-// The parsed out version of AuthenticatorAttestationResponse.
+// ParsedAttestationResponse is the parsed version of AuthenticatorAttestationResponse.
 type ParsedAttestationResponse struct {
 	CollectedClientData CollectedClientData
 	AttestationObject   AttestationObject
+	Transports          []AuthenticatorTransport
 }
 
-// From §6.4. Authenticators MUST also provide some form of attestation. The basic requirement is that the
-// authenticator can produce, for each credential public key, an attestation statement verifiable by the
-// WebAuthn Relying Party. Typically, this attestation statement contains a signature by an attestation
-// private key over the attested credential public key and a challenge, as well as a certificate or similar
-// data providing provenance information for the attestation public key, enabling the Relying Party to make
-// a trust decision. However, if an attestation key pair is not available, then the authenticator MUST
-// perform self attestation of the credential public key with the corresponding credential private key.
-// All this information is returned by authenticators any time a new public key credential is generated, in
-// the overall form of an attestation object. (https://www.w3.org/TR/webauthn/#attestation-object)
+// AttestationObject is the raw attestationObject.
+//
+// Authenticators SHOULD also provide some form of attestation, if possible. If an authenticator does, the basic
+// requirement is that the authenticator can produce, for each credential public key, an attestation statement
+// verifiable by the WebAuthn Relying Party. Typically, this attestation statement contains a signature by an
+// attestation private key over the attested credential public key and a challenge, as well as a certificate or similar
+// data providing provenance information for the attestation public key, enabling the Relying Party to make a trust
+// decision. However, if an attestation key pair is not available, then the authenticator MAY either perform self
+// attestation of the credential public key with the corresponding credential private key, or otherwise perform no
+// attestation. All this information is returned by authenticators any time a new public key credential is generated, in
+// the overall form of an attestation object.
+//
+// From: §6.5. Attestation
+// See: https://www.w3.org/TR/webauthn/#sctn-attestation
 type AttestationObject struct {
 	// The authenticator data, including the newly created public key. See AuthenticatorData for more info
 	AuthData AuthenticatorData
@@ -70,33 +73,30 @@ type attestationFormatValidationHandler func(AttestationObject, []byte) (string,
 
 var attestationRegistry = make(map[string]attestationFormatValidationHandler)
 
-// Using one of the locally registered attestation formats, handle validating the attestation
-// data provided by the authenticator (and in some cases its manufacturer)
+// RegisterAttestationFormat is a method to register attestation formats with the library. Generally using one of the
+// locally registered attestation formats is sufficient.
 func RegisterAttestationFormat(format string, handler attestationFormatValidationHandler) {
 	attestationRegistry[format] = handler
 }
 
 // Parse the values returned in the authenticator response and perform attestation verification
 // Step 8. This returns a fully decoded struct with the data put into a format that can be
-// used to verify the user and credential that was created
-func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse, error) {
-	var p ParsedAttestationResponse
+// used to verify the user and credential that was created.
+func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationResponse, err error) {
+	p = &ParsedAttestationResponse{}
 
-	err := json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData)
-	if err != nil {
+	if err = json.Unmarshal(ccr.ClientDataJSON, &p.CollectedClientData); err != nil {
 		return nil, ErrParsingData.WithInfo(err.Error())
 	}
 
-	err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject)
-	if err != nil {
+	if err = webauthncbor.Unmarshal(ccr.AttestationObject, &p.AttestationObject); err != nil {
 		return nil, ErrParsingData.WithInfo(err.Error())
 	}
 
 	// 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 attestation statement attStmt.
-	err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData)
-	if err != nil {
+	if err = p.AttestationObject.AuthData.Unmarshal(p.AttestationObject.RawAuthData); err != nil {
 		return nil, fmt.Errorf("error decoding auth data: %v", err)
 	}
 
@@ -104,19 +104,21 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (*ParsedAttestationResponse
 		return nil, ErrAttestationFormat.WithInfo("Attestation missing attested credential data flag")
 	}
 
-	return &p, nil
+	for _, t := range ccr.Transports {
+		p.Transports = append(p.Transports, AuthenticatorTransport(t))
+	}
+
+	return p, nil
 }
 
-// Verify - Perform Steps 9 through 14 of registration verification, delegating Steps
+// Verify performs Steps 9 through 14 of registration verification.
+//
+// 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 {
-	// 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
-
-	// Begin Step 9. Verify that the rpIdHash in authData is
-	// the SHA-256 hash of the RP ID expected by the RP.
 	rpIDHash := sha256.Sum256([]byte(relyingPartyID))
-	// Handle Steps 9 through 12
+
+	// 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)
 	if authDataVerificationError != nil {
 		return authDataVerificationError
@@ -138,6 +140,7 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client
 		if len(attestationObject.AttStatement) != 0 {
 			return ErrAttestationFormat.WithInfo("Attestation format none with attestation present")
 		}
+
 		return nil
 	}
 
@@ -154,11 +157,12 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client
 		return err.(*Error).WithInfo(attestationType)
 	}
 
-	uuid, err := uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID)
+	aaguid, err := uuid.FromBytes(attestationObject.AuthData.AttData.AAGUID)
 	if err != nil {
 		return err
 	}
-	if meta, ok := metadata.Metadata[uuid]; ok {
+
+	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")
@@ -166,26 +170,27 @@ func (attestationObject *AttestationObject) Verify(relyingPartyID string, client
 		}
 
 		if x5c != nil {
-			attestnCert, err := x509.ParseCertificate(x5c[0].([]byte))
+			x5cAtt, err := x509.ParseCertificate(x5c[0].([]byte))
 			if err != nil {
 				return ErrInvalidAttestation.WithDetails("Unable to parse attestation certificate from x5c")
 			}
-			if attestnCert.Subject.CommonName != attestnCert.Issuer.CommonName {
+
+			if x5cAtt.Subject.CommonName != x5cAtt.Issuer.CommonName {
 				var hasBasicFull = false
+
 				for _, a := range meta.MetadataStatement.AttestationTypes {
-					if metadata.AuthenticatorAttestationType(a) == metadata.BasicFull {
+					if a == metadata.BasicFull {
 						hasBasicFull = true
 					}
 				}
+
 				if !hasBasicFull {
 					return ErrInvalidAttestation.WithDetails("Attestation with full attestation from authentictor that does not support full attestation")
 				}
 			}
 		}
-	} else {
-		if metadata.Conformance {
-			return ErrInvalidAttestation.WithDetails(fmt.Sprintf("AAGUID %s not found in metadata during conformance testing", uuid.String()))
-		}
+	} 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_packed.go b/protocol/attestation_packed.go
index fb30984..c6f43e9 100644
--- a/protocol/attestation_packed.go
+++ b/protocol/attestation_packed.go
@@ -66,7 +66,7 @@ func verifyPackedFormat(att AttestationObject, clientDataHash []byte) (string, [
 	ecdaaKeyID, ecdaaKeyPresent := att.AttStatement["ecdaaKeyId"].([]byte)
 	if ecdaaKeyPresent {
 		// Handle ECDAA Attestation steps for the x509 Certificate
-		return handleECDAAAttesation(sig, clientDataHash, ecdaaKeyID)
+		return handleECDAAAttestation(sig, clientDataHash, ecdaaKeyID)
 	}
 
 	// Step 4. If neither x5c nor ecdaaKeyId is present, self attestation is in use.
@@ -195,7 +195,7 @@ func handleBasicAttestation(signature, clientDataHash, authData, aaguid []byte,
 	return string(metadata.BasicFull), x5c, nil
 }
 
-func handleECDAAAttesation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
+func handleECDAAAttestation(signature, clientDataHash, ecdaaKeyID []byte) (string, []interface{}, error) {
 	return "Packed (ECDAA)", nil, ErrNotSpecImplemented
 }
 
diff --git a/protocol/credential_test.go b/protocol/credential_test.go
index c06df12..8510b27 100644
--- a/protocol/credential_test.go
+++ b/protocol/credential_test.go
@@ -3,19 +3,17 @@ package protocol
 import (
 	"bytes"
 	"encoding/base64"
-	"io/ioutil"
-	"net/http"
-	"reflect"
+	"io"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
+
 	"github.com/go-webauthn/webauthn/protocol/webauthncbor"
 )
 
 func TestParseCredentialCreationResponse(t *testing.T) {
-	reqBody := ioutil.NopCloser(bytes.NewReader([]byte(testCredentialRequestBody)))
-	httpReq := &http.Request{Body: reqBody}
 	type args struct {
-		response *http.Request
+		responseName string
 	}
 
 	byteID, _ := base64.RawURLEncoding.DecodeString("6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g")
@@ -25,18 +23,18 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 	byteAttObject, _ := base64.RawURLEncoding.DecodeString("o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw")
 	byteClientDataJSON, _ := base64.RawURLEncoding.DecodeString("eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ")
 
-	tests := []struct {
-		name    string
-		args    args
-		want    *ParsedCredentialCreationData
-		wantErr bool
+	testCases := []struct {
+		name      string
+		args      args
+		expected  *ParsedCredentialCreationData
+		errString string
 	}{
 		{
-			name: "Successful Credential Request Parsing",
+			name: "ShouldParseCredentialRequest",
 			args: args{
-				response: httpReq,
+				responseName: "success",
 			},
-			want: &ParsedCredentialCreationData{
+			expected: &ParsedCredentialCreationData{
 				ParsedPublicKeyCredential: ParsedPublicKeyCredential{
 					ParsedCredential: ParsedCredential{
 						ID:   "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
@@ -47,7 +45,6 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 						"appid": true,
 					},
 				},
-				Transports: []AuthenticatorTransport{USB, NFC, "fake"},
 				Response: ParsedAttestationResponse{
 					CollectedClientData: CollectedClientData{
 						Type:      CeremonyType("webauthn.create"),
@@ -68,6 +65,67 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 							},
 						},
 					},
+					Transports: []AuthenticatorTransport{USB, NFC, "fake"},
+				},
+				Raw: CredentialCreationResponse{
+					PublicKeyCredential: PublicKeyCredential{
+						Credential: Credential{
+							Type: "public-key",
+							ID:   "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+						},
+						RawID: byteID,
+						ClientExtensionResults: AuthenticationExtensionsClientOutputs{
+							"appid": true,
+						},
+					},
+					AttestationResponse: AuthenticatorAttestationResponse{
+						AuthenticatorResponse: AuthenticatorResponse{
+							ClientDataJSON: byteClientDataJSON,
+						},
+						AttestationObject: byteAttObject,
+						Transports:        []string{"usb", "nfc", "fake"},
+					},
+				},
+			},
+			errString: "",
+		},
+		{
+			name: "ShouldParseCredentialRequestDeprecatedTransports",
+			args: args{
+				responseName: "successDeprecatedTransports",
+			},
+			expected: &ParsedCredentialCreationData{
+				ParsedPublicKeyCredential: ParsedPublicKeyCredential{
+					ParsedCredential: ParsedCredential{
+						ID:   "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+						Type: "public-key",
+					},
+					RawID: byteID,
+					ClientExtensionResults: AuthenticationExtensionsClientOutputs{
+						"appid": true,
+					},
+				},
+				Response: ParsedAttestationResponse{
+					CollectedClientData: CollectedClientData{
+						Type:      CeremonyType("webauthn.create"),
+						Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
+						Origin:    "https://webauthn.io",
+					},
+					AttestationObject: AttestationObject{
+						Format:      "none",
+						RawAuthData: byteAuthData,
+						AuthData: AuthenticatorData{
+							RPIDHash: byteRPIDHash,
+							Counter:  0,
+							Flags:    0x041,
+							AttData: AttestedCredentialData{
+								AAGUID:              make([]byte, 16),
+								CredentialID:        byteID,
+								CredentialPublicKey: byteCredentialPubKey,
+							},
+						},
+					},
+					Transports: []AuthenticatorTransport{USB, NFC, "fake"},
 				},
 				Raw: CredentialCreationResponse{
 					PublicKeyCredential: PublicKeyCredential{
@@ -89,60 +147,106 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 					Transports: []string{"usb", "nfc", "fake"},
 				},
 			},
-			wantErr: false,
+			errString: "",
+		},
+		{
+			name: "ShouldParseCredentialRequestDeprecatedTransportsShouldNotOverride",
+			args: args{
+				responseName: "successDeprecatedTransportsAndNew",
+			},
+			expected: &ParsedCredentialCreationData{
+				ParsedPublicKeyCredential: ParsedPublicKeyCredential{
+					ParsedCredential: ParsedCredential{
+						ID:   "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+						Type: "public-key",
+					},
+					RawID: byteID,
+					ClientExtensionResults: AuthenticationExtensionsClientOutputs{
+						"appid": true,
+					},
+				},
+				Response: ParsedAttestationResponse{
+					CollectedClientData: CollectedClientData{
+						Type:      CeremonyType("webauthn.create"),
+						Challenge: "W8GzFU8pGjhoRbWrLDlamAfq_y4S1CZG1VuoeRLARrE",
+						Origin:    "https://webauthn.io",
+					},
+					AttestationObject: AttestationObject{
+						Format:      "none",
+						RawAuthData: byteAuthData,
+						AuthData: AuthenticatorData{
+							RPIDHash: byteRPIDHash,
+							Counter:  0,
+							Flags:    0x041,
+							AttData: AttestedCredentialData{
+								AAGUID:              make([]byte, 16),
+								CredentialID:        byteID,
+								CredentialPublicKey: byteCredentialPubKey,
+							},
+						},
+					},
+					Transports: []AuthenticatorTransport{USB, NFC},
+				},
+				Raw: CredentialCreationResponse{
+					PublicKeyCredential: PublicKeyCredential{
+						Credential: Credential{
+							Type: "public-key",
+							ID:   "6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+						},
+						RawID: byteID,
+						ClientExtensionResults: AuthenticationExtensionsClientOutputs{
+							"appid": true,
+						},
+					},
+					AttestationResponse: AuthenticatorAttestationResponse{
+						AuthenticatorResponse: AuthenticatorResponse{
+							ClientDataJSON: byteClientDataJSON,
+						},
+						AttestationObject: byteAttObject,
+						Transports:        []string{"usb", "nfc"},
+					},
+					Transports: []string{"usb", "nfc", "fake"},
+				},
+			},
+			errString: "",
 		},
 	}
 
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := ParseCredentialCreationResponse(tt.args.response)
-			if (err != nil) != tt.wantErr {
-				t.Errorf("ParseCredentialCreationResponse() error = %v, wantErr %v", err, tt.wantErr)
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			body := io.NopCloser(bytes.NewReader([]byte(testCredentialRequestResponses[tc.args.responseName])))
+
+			actual, err := ParseCredentialCreationResponseBody(body)
+
+			if tc.errString != "" {
+				assert.EqualError(t, err, tc.errString)
+
 				return
 			}
-			if !reflect.DeepEqual(got.ClientExtensionResults, tt.want.ClientExtensionResults) {
-				t.Errorf("Extensions = %v \n want: %v", got.ClientExtensionResults, tt.want.ClientExtensionResults)
-			}
-			if !reflect.DeepEqual(got.Transports, tt.want.Transports) {
-				t.Errorf("Transports = %v \n want: %v", got.Transports, tt.want.Transports)
-			}
-			if !reflect.DeepEqual(got.ID, tt.want.ID) {
-				t.Errorf("ID = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.ParsedCredential, tt.want.ParsedCredential) {
-				t.Errorf("ParsedCredential = %v \n want: %v", got.ParsedCredential, tt.want.ParsedCredential)
-			}
-			if !reflect.DeepEqual(got.ParsedPublicKeyCredential, tt.want.ParsedPublicKeyCredential) {
-				t.Errorf("ParsedPublicKeyCredential = %v \n want: %v", got.ParsedPublicKeyCredential, tt.want.ParsedPublicKeyCredential)
-			}
-			if !reflect.DeepEqual(got.Raw, tt.want.Raw) {
-				t.Errorf("Raw = %v \n want: %v", got.Raw, tt.want.Raw)
-			}
-			if !reflect.DeepEqual(got.RawID, tt.want.RawID) {
-				t.Errorf("RawID = %v \n want: %v", got.RawID, tt.want.RawID)
-			}
+
+			assert.Equal(t, tc.expected.ClientExtensionResults, actual.ClientExtensionResults)
+			assert.Equal(t, tc.expected.ID, actual.ID)
+			assert.Equal(t, tc.expected.Type, actual.Type)
+			assert.Equal(t, tc.expected.ParsedCredential, actual.ParsedCredential)
+			assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
+			assert.Equal(t, tc.expected.ParsedPublicKeyCredential, actual.ParsedPublicKeyCredential)
+			assert.Equal(t, tc.expected.Raw, actual.Raw)
+			assert.Equal(t, tc.expected.RawID, actual.RawID)
+			assert.Equal(t, tc.expected.Response.Transports, actual.Response.Transports)
+			assert.Equal(t, tc.expected.Response.CollectedClientData, actual.Response.CollectedClientData)
+			assert.Equal(t, tc.expected.Response.AttestationObject.AuthData.AttData.CredentialID, actual.Response.AttestationObject.AuthData.AttData.CredentialID)
+			assert.Equal(t, tc.expected.Response.AttestationObject.Format, actual.Response.AttestationObject.Format)
+
 			// Unmarshall CredentialPublicKey
-			var pkWant interface{}
-			keyBytesWant := tt.want.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
-			webauthncbor.Unmarshal(keyBytesWant, &pkWant)
-			var pkGot interface{}
-			keyBytesGot := got.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
-			webauthncbor.Unmarshal(keyBytesGot, &pkGot)
-			if !reflect.DeepEqual(pkGot, pkWant) {
-				t.Errorf("Response = %+v \n want: %+v", pkGot, pkWant)
-			}
-			if !reflect.DeepEqual(got.Type, tt.want.Type) {
-				t.Errorf("Type = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.Response.CollectedClientData, tt.want.Response.CollectedClientData) {
-				t.Errorf("CollectedClientData = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.Response.AttestationObject.Format, tt.want.Response.AttestationObject.Format) {
-				t.Errorf("Format = %v \n want: %v", got, tt.want)
-			}
-			if !reflect.DeepEqual(got.Response.AttestationObject.AuthData.AttData.CredentialID, tt.want.Response.AttestationObject.AuthData.AttData.CredentialID) {
-				t.Errorf("CredentialID = %v \n want: %v", got, tt.want)
-			}
+			var pkExpected, pkActual interface{}
+
+			pkBytesExpected := tc.expected.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
+			assert.NoError(t, webauthncbor.Unmarshal(pkBytesExpected, &pkExpected))
+
+			pkBytesActual := actual.Response.AttestationObject.AuthData.AttData.CredentialPublicKey
+			assert.NoError(t, webauthncbor.Unmarshal(pkBytesActual, &pkActual))
+
+			assert.Equal(t, pkExpected, pkActual)
 		})
 	}
 }
@@ -243,7 +347,24 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) {
 	}
 }
 
-var testCredentialRequestBody = `{
+var testCredentialRequestResponses = map[string]string{
+	`success`: `
+{
+	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+	"type":"public-key",
+	"clientExtensionResults":{
+		"appid":true
+	},
+	"response":{
+		"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
+		"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
+		"transports":["usb","nfc","fake"]
+	}
+}
+`,
+	`successDeprecatedTransports`: `
+{
 	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"type":"public-key",
@@ -254,5 +375,23 @@ var testCredentialRequestBody = `{
 	"response":{
 		"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
 		"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
-		}
-	}`
+	}
+}
+`,
+	`successDeprecatedTransportsAndNew`: `
+{
+	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
+	"type":"public-key",
+	"transports":["usb","nfc","fake"],
+	"clientExtensionResults":{
+		"appid":true
+	},
+	"response":{
+		"attestationObject":"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjEdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQOsa7QYSUFukFOLTmgeK6x2ktirNMgwy_6vIwwtegxI2flS1X-JAkZL5dsadg-9bEz2J7PnsbB0B08txvsyUSvKlAQIDJiABIVggLKF5xS0_BntttUIrm2Z2tgZ4uQDwllbdIfrrBMABCNciWCDHwin8Zdkr56iSIh0MrB5qZiEzYLQpEOREhMUkY6q4Vw",
+		"clientDataJSON":"eyJjaGFsbGVuZ2UiOiJXOEd6RlU4cEdqaG9SYldyTERsYW1BZnFfeTRTMUNaRzFWdW9lUkxBUnJFIiwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdXRobi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ",
+		"transports":["usb","nfc"]
+	}
+}
+`,
+}