Skip to content

feat(principal)!: webcrypto ed25519 signer support for non-extractable agent keys#408

Open
NiKrause wants to merge 20 commits intostoracha:mainfrom
NiKrause:feat/658-webcrypto-ed25519-agent-keys
Open

feat(principal)!: webcrypto ed25519 signer support for non-extractable agent keys#408
NiKrause wants to merge 20 commits intostoracha:mainfrom
NiKrause:feat/658-webcrypto-ed25519-agent-keys

Conversation

@NiKrause
Copy link
Contributor

@NiKrause NiKrause commented Feb 11, 2026

Summary

  • switch @ucanto/principal Ed25519 signer implementation to WebCrypto
  • default Ed25519 generate() to non-extractable keys while preserving extractable mode when explicitly requested
  • remove direct @noble/ed25519 signing path from Ed25519 signer
  • add PKCS8/JWK conversion logic for Ed25519 derive/generate extractable flows
  • add coverage-focused tests for extractable/non-extractable behavior and error paths
  • update server receipt assertion to compare issuer by DID to avoid brittle object-shape comparison

Context

This aligns with storacha/upload-service#658, which requires browser agent key generation to move to non-extractable WebCrypto Ed25519 keys.

Breaking Changes

⚠️ Libs and tests which required extractable keys now have to explicitly request it with generate({ extractable: true })

Previously, keys were extractable by default. Now:

  • Default behavior: generate() produces non-extractable keys
  • Extractable keys: Must explicitly use generate({ extractable: true })
  • Impact: Code that relied on .secret() or .encode() access will need updates

Remark

Many changes are just prettier formatting changes


Co-Authored-By: NiKrause

Copy link
Member

@hannahhoward hannahhoward left a comment

Choose a reason for hiding this comment

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

Wow. So this is pretty great. Also great ed25519 FINALLY landed in webcrypto.... I followed but it was taking so long I lost track several times :P I think I'm good to approve it as is.

Just to confirm, re: extractable / non-extractable -- this isn't ultimately a breaking change cause the API.Signer returned by generate didn't provider access to the secret() method so you'd need a type cast for that anyway.

Also, does it make sense to rebuild the old ED25519Signer on an actual webcrypto key instead of importing on sign? Is there a perf cost to importing every time we need to sign? (I assuming not a significant one cause the compute cost comes from actually signing as opposed to importing)

Anyway, would love some color on the answers to these questions, but I am otherwise good to merge, and thank you so much for this contribution.

@NiKrause
Copy link
Contributor Author

NiKrause commented Feb 18, 2026

Thank you @hannahhoward for looking into this!

First of all, I enabled type checks on all branches ** and added the same typescript fixes from
#410 to this PR too otherwise they don't pass on the new failing main branch.

Otherwise I answer inline:

Just to confirm, re: extractable / non-extractable -- this isn't ultimately a breaking change cause the API.Signer returned by generate didn't provider access to the secret() method so you'd need a type cast for that anyway.

There's a good example here in the upload-service, which accessed and again accesses .secret directly from signer (ed25519.secret) see: https://github.com/storacha/upload-service/pull/667/changes#diff-e0788dca19d1076e1e2a937c09bf86711f6d4763400b47a743d88849b16abbbf
Also the previous Signer had a secret method: https://github.com/storacha/ucanto/blob/main/packages/principal/src/ed25519/signer.js#L155

Libs and tests which required extractable keys now have to enforce it with generate({ extractable: true }) the upload-service. That is probably a breaking change...

Also, does it make sense to rebuild the old ED25519Signer on an actual webcrypto key instead of importing on sign? Is there a perf cost to importing every time we need to sign? (I assuming not a significant one cause the compute cost comes from actually signing as opposed to importing)

We have now the "old" ED25519Signer and a new UnextractableEd25519Signer mainly for compatibility reasons.
If older code would still request .secret or .encode without enforcing it now with generate({ extractable: true }), it throws because private key bytes cannot be exported. Not so much because of performance improvements.

We could remove the ED25519Signer completely but e.g. the toArchive() function is needed elsewhere in other imlementing projects signing UCANs.

If we would use only one ED25519Signer, it would pretend it has access to .secret() and .encode(). Would be possible but seems worse design as of my research.

edited 2026-02-18 13:58 CET

@NiKrause NiKrause changed the title feat(principal): webcrypto ed25519 signer support for non-extractable agent keys feat(principal)!: webcrypto ed25519 signer support for non-extractable agent keys Feb 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants