Repository: go-webauthn/webauthn
Pull request #114 (2023-02-14 01:47:58)

fix(protocol): missing attachment field
The authenticatorAttachment field was missing previously from the PublicKeyCredentialJSON. This fixes it and includes it as part of the Credential during registration. See typedef https://w3c.github.io/webauthn/#typedefdef-publickeycredentialjson for more information.


Patch:
diff --git a/protocol/assertion.go b/protocol/assertion.go
index 38e4c6d..d6dcea3 100644
--- a/protocol/assertion.go
+++ b/protocol/assertion.go
@@ -78,9 +78,18 @@ func ParseCredentialRequestResponseBody(body io.Reader) (par *ParsedCredentialAs
 		return nil, ErrBadRequest.WithDetails("CredentialAssertionResponse with bad type")
 	}
 
+	var attachment AuthenticatorAttachment
+
+	switch car.AuthenticatorAttachment {
+	case "platform":
+		attachment = Platform
+	case "cross-platform":
+		attachment = CrossPlatform
+	}
+
 	par = &ParsedCredentialAssertionData{
 		ParsedPublicKeyCredential{
-			ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults,
+			ParsedCredential{car.ID, car.Type}, car.RawID, car.ClientExtensionResults, attachment,
 		},
 		ParsedAssertionResponse{
 			Signature:  car.AssertionResponse.Signature,
diff --git a/protocol/credential.go b/protocol/credential.go
index 8f03b71..887e2d7 100644
--- a/protocol/credential.go
+++ b/protocol/credential.go
@@ -32,14 +32,16 @@ type ParsedCredential struct {
 
 type PublicKeyCredential struct {
 	Credential
-	RawID                  URLEncodedBase64                      `json:"rawId"`
-	ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
+	RawID                   URLEncodedBase64                      `json:"rawId"`
+	ClientExtensionResults  AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
+	AuthenticatorAttachment string                                `json:"authenticatorAttachment,omitempty"`
 }
 
 type ParsedPublicKeyCredential struct {
 	ParsedCredential
-	RawID                  []byte                                `json:"rawId"`
-	ClientExtensionResults AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
+	RawID                   []byte                                `json:"rawId"`
+	ClientExtensionResults  AuthenticationExtensionsClientOutputs `json:"clientExtensionResults,omitempty"`
+	AuthenticatorAttachment AuthenticatorAttachment               `json:"authenticatorAttachment,omitempty"`
 }
 
 type CredentialCreationResponse struct {
@@ -97,9 +99,18 @@ func ParseCredentialCreationResponseBody(body io.Reader) (pcc *ParsedCredentialC
 		}
 	}
 
+	var attachment AuthenticatorAttachment
+
+	switch ccr.AuthenticatorAttachment {
+	case "platform":
+		attachment = Platform
+	case "cross-platform":
+		attachment = CrossPlatform
+	}
+
 	return &ParsedCredentialCreationData{
 		ParsedPublicKeyCredential{
-			ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults,
+			ParsedCredential{ccr.ID, ccr.Type}, ccr.RawID, ccr.ClientExtensionResults, attachment,
 		},
 		*response,
 		ccr,
diff --git a/webauthn/authenticator.go b/webauthn/authenticator.go
index ee88fb3..d23a4ce 100644
--- a/webauthn/authenticator.go
+++ b/webauthn/authenticator.go
@@ -1,7 +1,7 @@
 package webauthn
 
 import (
-	p "github.com/go-webauthn/webauthn/protocol"
+	"github.com/go-webauthn/webauthn/protocol"
 )
 
 type Authenticator struct {
@@ -18,14 +18,17 @@ type Authenticator struct {
 	// this information into their risk scoring. Whether the Relying Party updates the stored signature
 	// counter value in this case, or not, or fails the authentication ceremony or not, is Relying Party-specific.
 	CloneWarning bool
+
+	// Attachment is the authenticatorAttachment value returned by the request.
+	Attachment protocol.AuthenticatorAttachment
 }
 
 // Allow for easy marhsalling of authenticator options that are provided to the user
-func SelectAuthenticator(att string, rrk *bool, uv string) p.AuthenticatorSelection {
-	return p.AuthenticatorSelection{
-		AuthenticatorAttachment: p.AuthenticatorAttachment(att),
+func SelectAuthenticator(att string, rrk *bool, uv string) protocol.AuthenticatorSelection {
+	return protocol.AuthenticatorSelection{
+		AuthenticatorAttachment: protocol.AuthenticatorAttachment(att),
 		RequireResidentKey:      rrk,
-		UserVerification:        p.UserVerificationRequirement(uv),
+		UserVerification:        protocol.UserVerificationRequirement(uv),
 	}
 }
 
@@ -34,14 +37,14 @@ func SelectAuthenticator(att string, rrk *bool, uv string) p.AuthenticatorSelect
 // is nonzero or the value stored in conjunction with credential’s id attribute is nonzero, then
 // run the following sub-step:
 //
-//  If the signature counter value authData.signCount is
+//	If the signature counter value authData.signCount is
 //
-//  → Greater than the signature counter value stored in conjunction with credential’s id attribute.
-//  Update the stored signature counter value, associated with credential’s id attribute, to be the value of
-//  authData.signCount.
+//	→ Greater than the signature counter value stored in conjunction with credential’s id attribute.
+//	Update the stored signature counter value, associated with credential’s id attribute, to be the value of
+//	authData.signCount.
 //
-//  → Less than or equal to the signature counter value stored in conjunction with credential’s id attribute.
-//  This is a signal that the authenticator may be cloned, see CloneWarning above for more information.
+//	→ Less than or equal to the signature counter value stored in conjunction with credential’s id attribute.
+//	This is a signal that the authenticator may be cloned, see CloneWarning above for more information.
 func (a *Authenticator) UpdateCounter(authDataCount uint32) {
 	if authDataCount <= a.SignCount && (authDataCount != 0 || a.SignCount != 0) {
 		a.CloneWarning = true
diff --git a/webauthn/credential.go b/webauthn/credential.go
index 4b08dc9..282909e 100644
--- a/webauthn/credential.go
+++ b/webauthn/credential.go
@@ -39,8 +39,9 @@ func MakeNewCredential(c *protocol.ParsedCredentialCreationData) (*Credential, e
 		AttestationType: c.Response.AttestationObject.Format,
 		Transport:       c.Response.Transports,
 		Authenticator: Authenticator{
-			AAGUID:    c.Response.AttestationObject.AuthData.AttData.AAGUID,
-			SignCount: c.Response.AttestationObject.AuthData.Counter,
+			AAGUID:     c.Response.AttestationObject.AuthData.AttData.AAGUID,
+			SignCount:  c.Response.AttestationObject.AuthData.Counter,
+			Attachment: c.AuthenticatorAttachment,
 		},
 	}
 


Test patch:
diff --git a/protocol/credential_test.go b/protocol/credential_test.go
index 8510b27..5f7f223 100644
--- a/protocol/credential_test.go
+++ b/protocol/credential_test.go
@@ -44,6 +44,7 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 					ClientExtensionResults: AuthenticationExtensionsClientOutputs{
 						"appid": true,
 					},
+					AuthenticatorAttachment: Platform,
 				},
 				Response: ParsedAttestationResponse{
 					CollectedClientData: CollectedClientData{
@@ -77,6 +78,7 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 						ClientExtensionResults: AuthenticationExtensionsClientOutputs{
 							"appid": true,
 						},
+						AuthenticatorAttachment: "platform",
 					},
 					AttestationResponse: AuthenticatorAttestationResponse{
 						AuthenticatorResponse: AuthenticatorResponse{
@@ -137,6 +139,7 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 						ClientExtensionResults: AuthenticationExtensionsClientOutputs{
 							"appid": true,
 						},
+						AuthenticatorAttachment: "not-valid",
 					},
 					AttestationResponse: AuthenticatorAttestationResponse{
 						AuthenticatorResponse: AuthenticatorResponse{
@@ -164,6 +167,7 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 					ClientExtensionResults: AuthenticationExtensionsClientOutputs{
 						"appid": true,
 					},
+					AuthenticatorAttachment: CrossPlatform,
 				},
 				Response: ParsedAttestationResponse{
 					CollectedClientData: CollectedClientData{
@@ -197,6 +201,7 @@ func TestParseCredentialCreationResponse(t *testing.T) {
 						ClientExtensionResults: AuthenticationExtensionsClientOutputs{
 							"appid": true,
 						},
+						AuthenticatorAttachment: "cross-platform",
 					},
 					AttestationResponse: AuthenticatorAttestationResponse{
 						AuthenticatorResponse: AuthenticatorResponse{
@@ -353,6 +358,7 @@ var testCredentialRequestResponses = map[string]string{
 	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"type":"public-key",
+	"authenticatorAttachment":"platform",
 	"clientExtensionResults":{
 		"appid":true
 	},
@@ -368,6 +374,7 @@ var testCredentialRequestResponses = map[string]string{
 	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"type":"public-key",
+	"authenticatorAttachment":"not-valid",
 	"transports":["usb","nfc","fake"],
 	"clientExtensionResults":{
 		"appid":true
@@ -383,6 +390,7 @@ var testCredentialRequestResponses = map[string]string{
 	"id":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"rawId":"6xrtBhJQW6QU4tOaB4rrHaS2Ks0yDDL_q8jDC16DEjZ-VLVf4kCRkvl2xp2D71sTPYns-exsHQHTy3G-zJRK8g",
 	"type":"public-key",
+	"authenticatorAttachment":"cross-platform",
 	"transports":["usb","nfc","fake"],
 	"clientExtensionResults":{
 		"appid":true