Skip to content

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

Merged
sea-snake merged 7 commits into
masterfrom
sea-snake/cose-ed25519
May 12, 2026
Merged

feat(crypto): support Ed25519/EdDSA in COSE parser for WebAuthn#10081
sea-snake merged 7 commits into
masterfrom
sea-snake/cose-ed25519

Conversation

@sea-snake
Copy link
Copy Markdown
Contributor

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
Contributor 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

Comment thread rs/validator/src/webauthn.rs Outdated
Address review feedback from @eichhorl on #10081 by adding five
tests across the affected layers:

- rs/crypto/tests/request_id_signatures.rs: parse a COSE-DER-wrapped
  Ed25519 public key and assert both `AlgorithmId::Ed25519` and
  `KeyBytesContentType::Ed25519PublicKeyDerWrappedCose`.

- rs/validator/src/ingress_validation/tests.rs:
  validate_signature_webauthn_ed25519 — end-to-end validator check on
  `message_test_id(13)` signed by the RFC 8032 TEST 1 keypair.

- rs/validator/src/webauthn.rs (Ed25519 module):
  * incorrect public key (RFC 8032 TEST 2 pk) rejected with
    "Verifying signature failed.",
  * incorrect signature of correct length (64 zero bytes) rejected
    after the length check.

- rs/tests/crypto/ingress_verification_test.rs: extend the system test
  with a WebAuthnEd25519 identity type, including the COSE wrapping and
  signing helpers that mirror the existing ECDSA/RSA equivalents.
@sea-snake sea-snake requested a review from a team as a code owner May 11, 2026 14:38
@sea-snake
Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressing both threads:

Spec PR: dfinity/portal#6239. Adds EdDSA on curve Ed25519 to the WebAuthn allowed-schemes list, and clarifies that Ed25519 WebAuthn signatures are the raw 64-byte R || s concatenation from RFC 8032 §5.1.6, not DER-wrapped (which only applies to ECDSA).

Tests (commit edaadbc):

  • rs/crypto/tests/request_id_signatures.rs: should_correctly_parse_cose_encoded_der_wrapped_ed25519_pk asserts AlgorithmId::Ed25519 and KeyBytesContentType::Ed25519PublicKeyDerWrappedCose for a COSE-DER-wrapped Ed25519 pk.
  • rs/validator/src/ingress_validation/tests.rs: validate_signature_webauthn_ed25519 covers an end-to-end validator check on message_test_id(13) signed with the RFC 8032 TEST 1 keypair.
  • rs/validator/src/webauthn.rs (ed25519 mod):
    • should_return_error_on_incorrect_public_key uses RFC 8032 TEST 2 pk against a TEST 1 signature.
    • should_return_error_on_correct_length_but_invalid_ed25519_signature uses 64 zero bytes, which passes the length check and is then rejected by crypto verification.
  • rs/tests/crypto/ingress_verification_test.rs: extended with a WebAuthnEd25519 identity type, including the COSE wrapping and signing helpers that mirror the existing ECDSA/RSA equivalents.

sea-snake added 2 commits May 11, 2026 16:55
The basic_sig_from_webauthn_sig parameter was &&WebAuthnSignature, with
the call site passing &webauthn_sig where webauthn_sig was already a
&WebAuthnSignature. Rust's auto-deref made it work, but the extra layer
served no purpose. Take &WebAuthnSignature and pass webauthn_sig
directly.
CI's clippy run denies clippy::unseparated_literal_suffix on top of
clippy::all. Rewrite `0u8` as `0_u8` to satisfy the lint.
# Conflicts:
#	rs/crypto/internal/crypto_lib/basic_sig/cose/BUILD.bazel
@sea-snake sea-snake enabled auto-merge May 12, 2026 21:24
@sea-snake sea-snake added this pull request to the merge queue May 12, 2026
Merged via the queue into master with commit 3ef4334 May 12, 2026
37 checks passed
@sea-snake sea-snake deleted the sea-snake/cose-ed25519 branch May 12, 2026 22:46
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