Repository: go-webauthn/webauthn
Pull request #412 (2025-04-21 22:07:47)

feat(protocol): verify alg param during registration
feat(protocol): verify alg param during registration

This PR adds a check that is in the [webauthn spec 7.2](https://www.w3.org/TR/webauthn/#sctn-registering-a-new-credential) 
> 16. Verify that the "alg" parameter in the credential public key in authData matches the alg attribute of one of the items in options.pubKeyCredParams.

- Store `[]CredentialParameter` in session data 
- Add the verification logic into `AttestationObject.Verify()`
- Update step numbers in the comments to reflect the new values in the spec


coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->

> [!NOTE]
> Currently processing new changes in this PR. This may take a few minutes, please wait...
> 
> <details>
> <summary>📥 Commits</summary>
> 
> Reviewing files that changed from the base of the PR and between 109fafe96e53a0bc3a1573259f8c6c7be74d43bf and d580e38da1e218979b52b6f1a7a0eb9ad77c9950.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (7)</summary>
> 
> * `protocol/attestation.go` (3 hunks)
> * `protocol/attestation_test.go` (1 hunks)
> * `protocol/authenticator.go` (1 hunks)
> * `protocol/credential.go` (2 hunks)
> * `protocol/credential_test.go` (4 hunks)
> * `webauthn/registration.go` (2 hunks)
> * `webauthn/types.go` (1 hunks)
> 
> </details>
> 
> ```ascii
>  _________________________________________
> < Digging deeper than a Minecraft player. >
>  -----------------------------------------
>   \
>    \   \
>         \ /\
>         ( )
>       .( o ).
> ```

<!-- end of auto-generated comment: review in progress by coderabbit.ai -->
<!-- usage_tips_start -->

> [!TIP]
> <details>
> <summary>You can get early access to new features in CodeRabbit.</summary>
> 
> Enable the `early_access` setting in your project's settings in CodeRabbit to enable early access features such as new models, tools, and more.
> 
> </details>

<!-- usage_tips_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=go-webauthn/webauthn&utm_content=412):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index d3f1e49..e69b5f0 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -50,7 +50,7 @@ jobs:
 
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
+        uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
         with:
           languages: ${{ matrix.language }}
           # If you wish to specify custom queries, you can do so here or in a config file.
@@ -60,7 +60,7 @@ jobs:
       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
       # If this step fails, then you should remove it and run the build manually (see below)
       - name: Autobuild
-        uses: github/codeql-action/autobuild@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
+        uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
 
       # ℹ️ Command-line programs to run using the OS shell.
       # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -73,6 +73,6 @@ jobs:
       #   ./location_of_script_within_repo/buildscript.sh
 
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
+        uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
         with:
           category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml
index 30b9e52..6033b12 100644
--- a/.github/workflows/scorecards.yml
+++ b/.github/workflows/scorecards.yml
@@ -71,6 +71,6 @@ jobs:
 
       # Upload the results to GitHub's code scanning dashboard.
       - name: "Upload to code-scanning"
-        uses: github/codeql-action/upload-sarif@45775bd8235c68ba998cffa5171334d58593da47 # v3.28.15
+        uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3.28.16
         with:
           sarif_file: results.sarif
diff --git a/protocol/authenticator.go b/protocol/authenticator.go
index 4cd636a..c27726d 100644
--- a/protocol/authenticator.go
+++ b/protocol/authenticator.go
@@ -383,31 +383,31 @@ func ResidentKeyNotRequired() *bool {
 	return &required
 }
 
-// Verify on AuthenticatorData handles Steps 9 through 12 for Registration
-// and Steps 11 through 14 for Assertion.
+// Verify on AuthenticatorData handles Steps 13 through 15 & 17 for Registration
+// and Steps 15 through 18 for Assertion.
 func (a *AuthenticatorData) Verify(rpIdHash []byte, appIDHash []byte, userVerificationRequired bool) error {
 
-	// Registration Step 9 & Assertion Step 11
+	// Registration Step 13 & Assertion Step 15
 	// Verify that the RP ID hash in authData is indeed the SHA-256
 	// hash of the RP ID expected by the RP.
 	if !bytes.Equal(a.RPIDHash[:], rpIdHash) && !bytes.Equal(a.RPIDHash[:], appIDHash) {
 		return ErrVerification.WithInfo(fmt.Sprintf("RP Hash mismatch. Expected %x and Received %x", a.RPIDHash, rpIdHash))
 	}
 
-	// Registration Step 10 & Assertion Step 12
+	// Registration Step 14 & Assertion Step 16
 	// Verify that the User Present bit of the flags in authData is set.
 	if !a.Flags.UserPresent() {
 		return ErrVerification.WithInfo(fmt.Sprintln("User presence flag not set by authenticator"))
 	}
 
-	// Registration Step 11 & Assertion Step 13
+	// Registration Step 15 & Assertion Step 17
 	// If user verification is required for this assertion, verify that
 	// the User Verified bit of the flags in authData is set.
 	if userVerificationRequired && !a.Flags.UserVerified() {
 		return ErrVerification.WithInfo(fmt.Sprintln("User verification required but flag not set by authenticator"))
 	}
 
-	// Registration Step 12 & Assertion Step 14
+	// Registration Step 17 & Assertion Step 18
 	// Verify that the values of the client extension outputs in clientExtensionResults
 	// and the authenticator extension outputs in the extensions in authData are as
 	// expected, considering the client extension input values that were given as the
diff --git a/protocol/credential.go b/protocol/credential.go
index 7b18a1d..66f1537 100644
--- a/protocol/credential.go
+++ b/protocol/credential.go
@@ -145,7 +145,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, mds metadata.Provider) (clientDataHash []byte, err error) {
+func (pcc *ParsedCredentialCreationData) Verify(storedChallenge string, verifyUser bool, relyingPartyID string, rpOrigins, rpTopOrigins []string, rpTopOriginsVerify TopOriginVerificationMode, mds metadata.Provider, credParams []CredentialParameter) (clientDataHash []byte, err error) {
 	// Handles steps 3 through 6 - Verifying the Client Data against the Relying Party's stored data
 	if err = pcc.Response.CollectedClientData.Verify(storedChallenge, CreateCeremony, rpOrigins, rpTopOrigins, rpTopOriginsVerify); err != nil {
 		return nil, err
@@ -161,7 +161,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.
-	if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, mds); err != nil {
+	if err = pcc.Response.AttestationObject.Verify(relyingPartyID, clientDataHash, verifyUser, mds, credParams); err != nil {
 		return clientDataHash, err
 	}
 
diff --git a/webauthn/registration.go b/webauthn/registration.go
index be921be..bd16b3d 100644
--- a/webauthn/registration.go
+++ b/webauthn/registration.go
@@ -100,6 +100,7 @@ func (webauthn *WebAuthn) BeginMediatedRegistration(user User, mediation protoco
 		RelyingPartyID:   creation.Response.RelyingParty.ID,
 		UserID:           user.WebAuthnID(),
 		UserVerification: creation.Response.AuthenticatorSelection.UserVerification,
+		CredParams:       creation.Response.Parameters,
 	}
 
 	if webauthn.Config.Timeouts.Registration.Enforce {
@@ -233,7 +234,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.MDS); 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, session.CredParams); err != nil {
 		return nil, err
 	}
 
diff --git a/webauthn/types.go b/webauthn/types.go
index 5ce2f93..9e8ee8e 100644
--- a/webauthn/types.go
+++ b/webauthn/types.go
@@ -211,4 +211,5 @@ type SessionData struct {
 
 	UserVerification protocol.UserVerificationRequirement `json:"userVerification"`
 	Extensions       protocol.AuthenticationExtensions    `json:"extensions,omitempty"`
+	CredParams       []protocol.CredentialParameter       `json:"credParams,omitempty"`
 }


Test patch:
diff --git a/protocol/attestation.go b/protocol/attestation.go
index 55c76c6..11d9ce8 100644
--- a/protocol/attestation.go
+++ b/protocol/attestation.go
@@ -10,6 +10,7 @@ import (
 
 	"github.com/go-webauthn/webauthn/metadata"
 	"github.com/go-webauthn/webauthn/protocol/webauthncbor"
+	"github.com/go-webauthn/webauthn/protocol/webauthncose"
 )
 
 // AuthenticatorAttestationResponse is the initial unpacked 'response' object received by the relying party. This
@@ -119,25 +120,42 @@ func (ccr *AuthenticatorAttestationResponse) Parse() (p *ParsedAttestationRespon
 	return p, nil
 }
 
-// Verify performs Steps 9 through 14 of registration verification.
+// Verify performs Steps 13 through 19 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
+// Steps 13 through 15 are verified against the auth data. These steps are identical to 15 through 18 for assertion so we
 // handle them with AuthData.
-func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, mds metadata.Provider) (err error) {
+func (a *AttestationObject) Verify(relyingPartyID string, clientDataHash []byte, userVerificationRequired bool, mds metadata.Provider, credParams []CredentialParameter) (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.
+	// Begin Step 13 through 15. Verify that the rpIdHash in authData is the SHA-256 hash of the RP ID expected by the RP.
 	if err = a.AuthData.Verify(rpIDHash[:], nil, userVerificationRequired); err != nil {
 		return err
 	}
 
+	// Step 16. Verify that the "alg" parameter in the credential public key in
+	// authData matches the alg attribute of one of the items in options.pubKeyCredParams.
+	pk := webauthncose.PublicKeyData{}
+	if err = webauthncbor.Unmarshal(a.AuthData.AttData.CredentialPublicKey, &pk); err != nil {
+		return err
+	}
+	found := false
+	for _, credParam := range credParams {
+		if int(pk.Algorithm) == int(credParam.Algorithm) {
+			found = true
+			break
+		}
+	}
+	if !found {
+		return ErrAttestationFormat.WithInfo("Credential public key algorithm not supported")
+	}
+
 	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
+	// Step 18. 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
 	// list of registered WebAuthn Attestation Statement Format Identifier
@@ -172,7 +190,7 @@ func (a *AttestationObject) VerifyAttestation(clientDataHash []byte, mds metadat
 		x5cs            []any
 	)
 
-	// Step 14. Verify that attStmt is a correct attestation statement, conveying a valid attestation signature, by using
+	// Step 19. 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.
 	if attestationType, x5cs, err = handler(*a, clientDataHash, mds); err != nil {
diff --git a/protocol/attestation_test.go b/protocol/attestation_test.go
index 86caaf4..8ccb1f6 100644
--- a/protocol/attestation_test.go
+++ b/protocol/attestation_test.go
@@ -29,7 +29,7 @@ func TestAttestationVerify(t *testing.T) {
 
 			pcc.Response = *parsedAttestationResponse
 
-			_, err = 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, options.Response.Parameters)
 
 			require.NoError(t, err)
 		})
diff --git a/protocol/credential_test.go b/protocol/credential_test.go
index 17febb1..4068e51 100644
--- a/protocol/credential_test.go
+++ b/protocol/credential_test.go
@@ -9,6 +9,7 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/go-webauthn/webauthn/protocol/webauthncbor"
+	"github.com/go-webauthn/webauthn/protocol/webauthncose"
 )
 
 func TestParseCredentialCreationResponse(t *testing.T) {
@@ -168,6 +169,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) {
 		verifyUser         bool
 		relyingPartyID     string
 		relyingPartyOrigin []string
+		credParams         []CredentialParameter
 	}
 
 	tests := []struct {
@@ -228,6 +230,7 @@ func TestParsedCredentialCreationData_Verify(t *testing.T) {
 				verifyUser:         false,
 				relyingPartyID:     `webauthn.io`,
 				relyingPartyOrigin: []string{`https://webauthn.io`},
+				credParams:         []CredentialParameter{{Type: "public-key", Algorithm: webauthncose.AlgES256}},
 			},
 			wantErr: false,
 		},
@@ -240,7 +243,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, tt.args.credParams); (err != nil) != tt.wantErr {
 				t.Errorf("ParsedCredentialCreationData.Verify() error = %+v, wantErr %v", err, tt.wantErr)
 			}
 		})