Skip to content

feat(crypto): support Ed25519/EdDSA in COSE parser for WebAuthn#10081

Open
sea-snake wants to merge 2 commits intomasterfrom
sea-snake/cose-ed25519
Open

feat(crypto): support Ed25519/EdDSA in COSE parser for WebAuthn#10081
sea-snake wants to merge 2 commits intomasterfrom
sea-snake/cose-ed25519

Conversation

@sea-snake
Copy link
Copy Markdown

Problem

The IC's COSE parser at rs/crypto/internal/crypto_lib/basic_sig/cose/src/lib.rs only accepts ECDSA-P256 (kty=EC2, alg=ES256) and RSA-PKCS1-SHA256 (kty=RSA, alg=RS256) keys. WebAuthn authenticators that produce kty=OKP, alg=EdDSA, crv=Ed25519 keys (e.g. NitroKey 3A) are rejected with the misleading error:

Invalid delegation: Invalid public key: Algorithm Unspecified not supported: Algorithm not supported in COSE parser

The "Unspecified" comes from the wrapper at parse_cose_public_key, which hardcodes AlgorithmId::Unspecified for any AlgorithmNotSupported outcome — so the actual EdDSA alg value never surfaces.

Why Ed25519 recovery phrases work but Ed25519 WebAuthn keys don't

The IC already has full Ed25519 support for non-WebAuthn keys. Recovery phrases in Internet Identity generate raw Ed25519 keys that are DER-encoded with the standard Ed25519 algorithm identifier (RFC 8410, OID 1.3.101.112). These take the ed25519_algorithm_identifier() path in user_public_key_from_bytes and are verified via verify_basic_sig_by_public_key(AlgorithmId::Ed25519, ...) with a plain signature — no WebAuthn envelope, no COSE parsing.

WebAuthn authenticators like the NitroKey 3A take a different code path: the public key is COSE-encoded and DER-wrapped with the IC's COSE OID (1.3.6.1.4.1.56387.1.1). This hits the cose_algorithm_identifier() branch, which calls parse_cose_public_key — and that parser only knew ECDSA-P256 and RSA-SHA256. The Ed25519 cryptographic primitives were always there; the COSE parser just never learned to unwrap Ed25519/EdDSA keys from the COSE map format that WebAuthn produces.

This was reported via dfinity/internet-identity#3835 with a captured COSE key from a NitroKey 3A. The captured key has only the standard 4 COSE map entries (no key_ops or other extension fields), confirming the root cause is the parser's algorithm allowlist, not extra entries.

Changes

cose crate

  • Add CosePublicKey::Ed25519(Vec<u8>) variant.
  • Add parse_eddsa_ed25519 helper that decodes kty=OKP / alg=EdDSA / crv=Ed25519 / x=<32 bytes> and returns the RFC 8410 SPKI DER via ic_ed25519::PublicKey::deserialize_raw().serialize_rfc8410_der().
  • Wire the new branch into CosePublicKey::from_cbor.
  • Add ic-ed25519 to the crate's Cargo.toml and BUILD.bazel.

ic-crypto-standalone-sig-verifier

  • Add KeyBytesContentType::Ed25519PublicKeyDerWrappedCose.
  • Map AlgorithmId::Ed25519 to it in cose_key_bytes_content_type.

ic-validator

  • ingress_validation.rs: include Ed25519PublicKeyDerWrappedCose in the WebAuthn signature path (both in validate_signed_request and validate_signed_delegation).
  • webauthn.rs: add an AlgorithmId::Ed25519 arm to basic_sig_from_webauthn_sig. Per WebAuthn §6.5.6, EdDSA WebAuthn signatures are 64 raw bytes (no DER wrapping) — they pass through unchanged.

Tests

  • cose/tests/tests.rs: parsing of the RFC 8032 TEST 1 keypair as a COSE key, end-to-end signature verification through the parsed DER, parsing of the captured NitroKey 3A WebAuthn key, rejection of crv=Ed448 and short-x malformed keys.
  • validator/src/webauthn.rs: a new ed25519 test module mirroring the existing ecdsa and rsa modules, with a valid-signature, wrong-message, and malformed-signature test. Updates the existing should_return_error_if_algorithm_id_is_not_supported test (which previously asserted Ed25519 was unsupported) to use AlgorithmId::EcdsaSecp256k1 instead.

Verification

cargo test -p ic-crypto-internal-basic-sig-cose -p ic-crypto-standalone-sig-verifier -p ic-validator
cargo clippy --all-features --tests -p ic-crypto-internal-basic-sig-cose -p ic-crypto-standalone-sig-verifier -p ic-validator -- -D warnings -D clippy::all

All clean locally. Bazel build/test was not run in the development environment — please verify in CI.

Related

The COSE parser only accepted ECDSA-P256 (kty=EC2, alg=ES256) and
RSA-PKCS1-SHA256 (kty=RSA, alg=RS256). WebAuthn authenticators that
produce kty=OKP, alg=EdDSA, crv=Ed25519 keys (e.g. NitroKey 3A) were
rejected with the misleading error "Algorithm Unspecified not supported"
- the wrapper hardcodes AlgorithmId::Unspecified for any unsupported
algorithm, so the actual Ed25519 alg never surfaces.

Add an Ed25519/EdDSA branch to CosePublicKey::from_cbor and a
parse_eddsa_ed25519 helper that produces an RFC 8410 SPKI DER. Wire
Ed25519PublicKeyDerWrappedCose through the standalone sig verifier and
the ingress validator's WebAuthn signature path. Per WebAuthn §6.5.6,
EdDSA WebAuthn signatures are 64 raw bytes (no DER wrapping), which
basic_sig_from_webauthn_sig now passes through unchanged.

Tests cover: parsing the canonical RFC 8032 TEST 1 keypair as a COSE
key, end-to-end signature verification through the parsed DER, parsing
a captured NitroKey 3A WebAuthn registration, and rejection of
Ed448 (crv=7) and short-x malformed keys. The validator's webauthn
module gets matching Ed25519 fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions github-actions Bot added the feat label May 4, 2026
@sea-snake sea-snake requested a review from Copilot May 4, 2026 13:58
@sea-snake sea-snake marked this pull request as ready for review May 4, 2026 13:59
@sea-snake sea-snake requested a review from a team as a code owner May 4, 2026 13:59
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the IC’s WebAuthn COSE public-key parsing/verification pipeline to accept Ed25519 (COSE kty=OKP, alg=EdDSA, crv=Ed25519) in addition to the existing ECDSA-P256 and RSA-SHA256 support, enabling authenticators that emit Ed25519 COSE keys (e.g., NitroKey 3A).

Changes:

  • Add Ed25519/EdDSA parsing to the COSE key parser and return RFC 8410 SPKI DER for the embedded key.
  • Thread a new COSE-wrapped Ed25519 key content type through ic-crypto-standalone-sig-verifier and ic-validator to route such keys through the WebAuthn verification path.
  • Add end-to-end tests for parsing and verifying Ed25519 WebAuthn signatures, plus negative cases for unsupported curves/malformed keys/signatures.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
rs/crypto/internal/crypto_lib/basic_sig/cose/src/lib.rs Add COSE OKP/EdDSA/Ed25519 parsing and map it to AlgorithmId::Ed25519 + RFC 8410 DER output.
rs/crypto/internal/crypto_lib/basic_sig/cose/tests/tests.rs Add COSE Ed25519 parsing + verification tests (including NitroKey capture and malformed/unsupported inputs).
rs/crypto/internal/crypto_lib/basic_sig/cose/Cargo.toml Add ic-ed25519 dependency for Ed25519 key decoding/DER serialization.
rs/crypto/internal/crypto_lib/basic_sig/cose/BUILD.bazel Add Bazel dependency on //packages/ic-ed25519.
rs/crypto/standalone-sig-verifier/src/sign_utils.rs Introduce KeyBytesContentType::Ed25519PublicKeyDerWrappedCose and map Ed25519 COSE-unwrapped keys to it.
rs/validator/src/ingress_validation.rs Treat Ed25519 COSE-wrapped keys as WebAuthn keys in ingress/delegation validation.
rs/validator/src/webauthn.rs Accept Ed25519 in WebAuthn signature handling and add validator-level Ed25519 WebAuthn tests.
Cargo.lock Record the additional ic-ed25519 dependency in the lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread rs/validator/src/webauthn.rs Outdated
Address Copilot review on PR #10081:

- `webauthn_sig.signature()` already returns an owned cloned `Blob`, so
  `.0.clone()` was an extra Vec clone. Move the Vec out instead with
  `webauthn_sig.signature().0`.
- WebAuthn requires Ed25519 signatures to be exactly 64 bytes
  (W3C WebAuthn-2 §6.5.6). Reject other lengths early in the validator
  with a clear error rather than relying on the crypto verifier's
  generic "Invalid length" message.

Tests: tighten the malformed-signature test to assert the new error
message text instead of just `is_err()`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@sea-snake
Copy link
Copy Markdown
Author

Addressed Copilot review (99ee480):

  • Dropped the redundant .clone()webauthn_sig.signature() already returns an owned Blob, so .0 moves the Vec out without a second allocation.
  • Added an explicit 64-byte length check with a clear error message (Invalid Ed25519 signature length: expected 64 bytes, got N), so malformed signatures are caught early in the validator rather than surfacing a generic error from the crypto layer.
  • Tightened the malformed-signature test to assert the new error text.

Copy link
Copy Markdown
Contributor

@eichhorl eichhorl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @sea-snake, is there a corresponding interface spec PR?

Some ideas for more tests:

  • Extend rs/validator/src/ingress_validation/tests.rs with a test validating Ed25519
  • Incorrect public key should be rejected in rs/validator/src/webauthn.rs
  • Incorrect signature with correct length should be rejected in rs/validator/src/webauthn.rs
  • Parsing of the new Ed25519PublicKeyDerWrappedCose variant in rs/crypto/tests/request_id_signatures.rs
  • Extend the rs/tests/crypto/ingress_verification_test.rs system test with the new scheme

}

fn basic_sig_from_webauthn_sig(
webauthn_sig: &&WebAuthnSignature,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not your change, but does this work?

Suggested change
webauthn_sig: &WebAuthnSignature,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants