Skip to content

feat: ECDH-ES over any registered key type, X25519/X448, and opt-in ES256K - #532

Draft
polewskm wants to merge 14 commits into
mainfrom
claude/jose-ecdh-okp-es256k
Draft

polewskm wants to merge 14 commits into
mainfrom
claude/jose-ecdh-okp-es256k

Conversation

@polewskm

@polewskm polewskm commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

The JOSE algorithm model lets one algorithm name serve every key type it declares. On that model, ECDH-ES works over any registered key type, X25519 and X448 are available, and ES256K is an opt-in package.

  • Let ECDH-ES use an ephemeral key of any registered key type #327: one algorithm name, several key types (ADR-1389).
    • KeyedAlgorithm.KeyTypes lists AlgorithmKeyType entries in order of preference. Each entry gives a secret type, its sizes and its curves; a key type bound to curves takes its sizes from them (AlgorithmKeyType.OnCurves).
    • ECDH-ES takes its key types from the registered IEcdhKeyAgreements. The core serves P-256, P-384 and P-521.
    • The epk is written and read through the key type's own descriptors: only its required public members, never a private one.
    • ECDH-ES+A*KW generates a random content encryption key and wraps it under the key agreed with the epk (RFC 7518 §4.6).
    • The algorithms capability lists each compatible secret type with its sizes and curves, and the console offers exactly those.
  • ECDH-ES over X25519 and X448 (RFC 8037) #336: ECDH-ES over X25519 and X448 (ADR-1390).``
    • AddEdDsa() registers clean-room RFC 7748 agreements: a constant-time Montgomery ladder that refuses an all-zero shared secret, tested against the RFC 7748 and RFC 8037 vectors.
    • A curve's use keeps the X25519/X448 OKP keys apart from the Ed25519/Ed448 signing keys.
  • Opt-in ES256K (RFC 8812) package with a secp256k1 curve #337: ES256K as an opt-in package (ADR-1392).```
    • NCode.Identity.Jose.Secp256k1 provides AddSecp256k1() on IServiceBuilder<JoseLibrary>.
    • It registers the curve and an EccSignatureAlgorithmSpecification as data, and registers nothing where the platform lacks the curve.
  • Validating and creating keys:
    • A declared key's type, size and curve come from the key itself.
    • IAlgorithmCollection.TryResolveKeyGeneration places every generated key on one of the algorithm's curves, and is shared by tenant provisioning and management create.
    • ISecretValidator.ValidateCreate returns the resolved SecretKeyGenerationParameters.
    • A tenant key entry can name its SecretType and Curve.

Closes #327
Closes #336
Closes #337

A keyed algorithm declares a list of key types (KeyedAlgorithm.KeyTypes of
AlgorithmKeyType), so one algorithm name serves several key types, as RFC 8037
section 3.2 needs for ECDH-ES over EC and OKP keys. The ECDH-ES algorithms take
their key types from registered IEcdhKeyAgreement strategies; the core registers
the EC agreement over P-256, P-384 and P-521. The epk is written and read through
the key type's ISecretKeyTypeDescriptor and JSON Web Key rules: only the type's
required members are written, the kty must be the recipient key's, and a received
epk holding any private member is refused.

The JWE decryption key check, ValidateKeyAlgorithm, the client authentication
and decryption policies, tenant key provisioning (with an optional SecretType)
and the management capabilities document (KeyTypes per secret type) read the
key types. ECDH-ES+A*KW now wraps a random content encryption key, and AEAD
algorithms are no longer keyed algorithms. ADR-1389 records the decision.

Closes #327
NCode.Identity.Jose.EdDsa implements the X25519 and X448 functions of RFC 7748
clean-room, as constant-time Montgomery ladders over its existing field
arithmetic, and AddEdDsa() registers them as the OKP agreement of ECDH-ES,
ECDH-ES+A128KW, ECDH-ES+A192KW and ECDH-ES+A256KW (RFC 8037 section 3.2).

The OKP key type generates, imports, exports and stores crv X25519 and X448 keys
as it does the signing keys. The curve catalog records each curve's use, so an
X25519 or X448 key never signs and an Ed25519 or Ed448 key never agrees,
although X25519 and Ed25519 keys have the same size. An all-zero shared secret,
from an epk or a recipient key of small order, is refused on both sides.

Tests cover the RFC 7748 section 5.2 and section 6 vectors (iterated to 1,000),
the RFC 8037 Appendix A.6 example, the RFC 8410 private key example, a
differential oracle, and JWE round trips over each curve and key wrap.
ADR-1390 records the decision.

Closes #336
NCode.Identity.Jose.Secp256k1 adds ES256K (RFC 8812): AddSecp256k1()
registers the secp256k1 curve (crv "secp256k1") in the elliptic-curve
catalog and an ES256K ECDSA algorithm bound to it, so a P-256 key never
signs as ES256K and a secp256k1 key never signs as ES256. Registration
probes the platform first and registers nothing where its cryptography
lacks the curve, as on macOS.

An ECDSA algorithm is data: an EccSignatureAlgorithmSpecification
service names its code, curve and hash, and the JOSE library serves one
algorithm per specification beside ES256, ES384 and ES512, failing at
startup on an empty or duplicate code. AlgorithmKeyType.Curve names the
curve an algorithm is bound to, so tenant key provisioning generates
ES256K, Ed25519 and Ed448 keys on their curves, and a stored key
satisfies an algorithm only when the algorithm accepts its public half.

Recorded in ADR-1391, which amends ADR-1169, ADR-1272 and ADR-1390.

Closes #337
- Check a key's curve when it is declared for an algorithm: AlgorithmKeyType
  lists its curves as AlgorithmCurve (crv and key size), KeyAlgorithmDeclaration
  carries the curve and, where the caller holds it, the key, and the client key
  importer and the management update pass the key, so an Ed25519 key for
  ECDH-ES or a P-256 key for ES256K is refused at registration.
- Report a JWE decryption key on a curve the algorithm refuses as the caller's
  configuration error.
- Choose the ECDH-ES agreement per key, merge agreements that serve one key
  type, and order key types independently of registration order.
- Provision tenant keys and management keys created without a curve on the
  first of the algorithm's curves with the key's size, so OKP keys for ECDH-ES
  and EdDSA can be provisioned; list each key type's curves in the algorithms
  capability and offer only those in the console.
- Test hygiene: Linux-only fact, region names, one behaviour per test, an
  honest label for the X25519 PKCS#8 input, and doc comments on the X25519 and
  X448 constants.
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.55752% with 7 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
...Identity.Jose.Secp256k1/KeyTypes/Secp256k1Curve.cs 88.88% 2 Missing ⚠️
...Authentication/Clients/DefaultClientKeyImporter.cs 83.33% 2 Missing ⚠️
...ment/Endpoints/Clients/ClientApiEndpointHandler.cs 87.50% 1 Missing ⚠️
...esourceServers/ResourceServerApiEndpointHandler.cs 75.00% 1 Missing ⚠️
...gement/Endpoints/Secrets/DefaultSecretValidator.cs 98.11% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #532      +/-   ##
==========================================
+ Coverage   92.09%   92.28%   +0.18%     
==========================================
  Files        1495     1518      +23     
  Lines       62761    63825    +1064     
  Branches      306      305       -1     
==========================================
+ Hits        57798    58899    +1101     
+ Misses       4963     4926      -37     
Flag Coverage Δ
dotnet 91.95% <99.54%> (+0.19%) ⬆️
frontend 99.09% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

📢 Thoughts on this report? Let us know!

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ncode-review ncode-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 NCode Review at 0af7f02: 31 new findings on the changed lines.

Not on a changed line:

  • 🟡 F28 Nit: Summary line exceeds the wrap width src/NCode.Identity.OpenId.Management/Endpoints/Capabilities/AlgorithmCapabilitiesProvider.cs:30. The reworded 'with its implied use and each compatible secret type...' line runs past the width the rest of the comment wraps at. Fix: Rewrap the summary at the same column as the other lines.

Comment thread CHANGELOG.md Resolved
@ncode-review

ncode-review Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Superseded by the review at df1e680.

@polewskm polewskm changed the title feat!: ECDH-ES over any registered key type, X25519/X448, and opt-in ES256K feat: ECDH-ES over any registered key type, X25519/X448, and opt-in ES256K Nov 3, 2026
An asymmetric key names its curve (AsymmetricSecretKey.CurveName, from the
elliptic-curve catalog for ecc keys), and AlgorithmKeyType.IsKeyCompatible
is the one rule algorithms and ECDH agreements check a key's type, size and
curve by. AlgorithmKeyType.OnCurves takes its sizes from its curves.

A KeyAlgorithmDeclaration carries the key itself, and a key to generate is
resolved onto a curve by TryResolveKeyGeneration, which tenant provisioning
and management creation share; ValidateCreate returns the generation
parameters, and a tenant key may name its Curve. A stored tenant key without
a readable public half does not satisfy a specification.

ECDH agreements keep registration order, the wrapping algorithm agrees its
KEK through a named helper, ES256/ES384/ES512 are registered as data and
every ECDSA specification's curve must be in the catalog, AddSecretsLibrary
registers logging, and the OKP curves are objects of their kind. The console
offers discovered secret types and every curve an algorithm lists.
…follow-up)

A tenant key specification's curve is part of the key it names: a stored key
satisfies it only when its public half is on that curve, so a configured
X448 or Brainpool curve is provisioned beside an X25519 or P-256 key.

TryResolveKeyGeneration refuses a curve declared for a key type the
algorithm names no curve for, and ResolvedKeyGeneration.Curve is null for
such a type. A held key of a type without curves whose material is not at
hand, such as a stored symmetric key, is checked by ValidateKeySize over a
KeySizeDeclaration, which refuses a curve-bound key type.

GenerateSecretRequest carries the validated SecretKeyGenerationParameters
whole as GenerationParameters, so no handler copies the size and curve
field by field; a shared secret refuses a curve.

The ECDH-ES algorithms refuse agreements of one key type that mix curves
with sizes only, so the merged key type always lists its curves.

The console offers only the discovered secret types, and a secret type with
curves takes a curve, the type's first being chosen with it.

Docs: OpenIdTenantKeyOptions.KeySizeBits names the algorithm's first curve,
ISecretValidator.ValidateUpdate describes the public-half check, the ECDSA
specification checks run when the algorithm collection is built, ADR-1390
and ADR-1392 state their context without narrating it, and the index lists
what ADR-1389 amends.
…es256k

# Conflicts:
#	docs/decisions/README.md

polewskm commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Autopilot: The NCode Review finding that isn't on a changed line:

  • F28 (fixed): Rewrapped the AlgorithmCapabilitiesProvider summary at the comment's width; it now also mentions the curves the section lists. The DefaultRegistration summaries of NCode.Identity.Jose.EdDsa and NCode.Identity.Jose.Secp256k1, which the builder change had pushed past the width, are rewrapped the same way.

Generated by Claude Code

polewskm commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Autopilot: Two checks are red at df1e680, and both are this PR's to fix.

  • Commit attribution: the PR description had picked up a "Generated by" footer. The description is corrected, and editing it re-runs the check.
  • Lint, typecheck, test, build: the console's secret form now requires a curve for secret types that offer curves, and three apps/mgmt container tests haven't caught up. The console guard catches warnings in their secret-creation flows, and ResourceServerDetailPage finds duplicate elements. On top of that, branch coverage in packages/ui dropped to 96.89%, under its 97% floor. The fix updates the components and containers and adds tests for the new curve branches. It goes in the next push, with the frontend suites run locally first. The coverage floor stays as it is.

Generated by Claude Code

@polewskm

polewskm commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

/review

@ncode-review

ncode-review Bot commented Nov 3, 2026

Copy link
Copy Markdown

🤖 Review complete

Verdict: ✅ Approve — Deep review (sensitive paths): Re-review of what changed since the last review at 0af7f02: 511 hunks in 137 files and changes taken back in 83 files, by correctness and security. No further findings above nits.

2 reviewers run — 0 findings (run details)

Reviewer Model Findings Top
correctness opus-5-5 0 🟢
security opus-5-5 0 🟢

Trial: findings are posted as review threads and in the check run. $3.33 on Amazon Bedrock, 31s, at df1e680. (how to interact)

…curve selection

The secret-type select held the seeded `symmetric` while the discovered type list was still empty, which MUI reports
as an out-of-range value. The form now selects only a type it offers: the seeded type when offered, otherwise the
first offered type, and none (with submit blocked) while the types load. A selected curve now fixes the key size by
derivation rather than by copying it into state.

SecretsManager forwarded the curve only for `ecc`, so an `okp` secret lost the curve its key type requires; it now
forwards the form's curve for every secret type with curves.

The container tests that create a secret now discover a secret type and wait for the form to offer it.
…es256k

# Conflicts:
#	CHANGELOG.md
#	src/NCode.Identity.Jose/Algorithms/KeyManagement/Pbes2KeyManagementAlgorithm.cs

polewskm commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Autopilot: Both fixes are pushed. CI was green on d703fc1, and it reruns now on 56e7835.

  • d703fc1: the console's secret form only uses a secret type it offers. While the discovered types load, it holds an empty value instead of symmetric, which was the out-of-range value MUI warned about. The containers send an OKP secret's curve. New tests bring packages/ui branch coverage to 97.52%.
  • 56e7835: merges main. This PR's changelog entries are recast into main's catalog, and the conflict in Pbes2KeyManagementAlgorithm resolves to this PR's StaticKeyBitSizes.

Generated by Claude Code

Cover the refusals of the EC and OKP agreements called directly (a
shared-secret buffer of the wrong size, a recipient on a curve or of a
size the agreement does not serve, a public-only recipient, an
ephemeral key of another type), the ECDH-ES algorithm's refusals of a
key type without a JSON Web Key form, of an ephemeral key its key type
cannot read, of a contributed key type whose required members name a
private member or are missing from its export, and of a malformed apu
or apv, the merge of size-only agreements, and the matching gaps in
AlgorithmCollectionExtensions, DefaultTenantKeyPolicy, the OKP curves
and keys, and the X25519 and X448 guards.

Remove two dead branches: DecodePartyInfo decodes through
Base64Url.Decode, since TryDecode into a buffer of the decoded size
either throws or fills it, and TryResolveKeyGeneration reports the
declared size alone, since a curve's own size is read only when it
was not found.

polewskm commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Autopilot: bbf9977 covers the refusal paths Codecov reported as untested:

  • the ECDH-ES epk checks for a wrong kty, a missing or foreign member, and a private member;
  • a key whose curve no agreement accepts, and ambiguous agreements;
  • shared-secret buffers that are too small, and a wrong-size shared secret;
  • the X25519/X448 length guards;
  • tenant key provisioning with no acceptable secret type.

Every flagged file except MlDsaSignatureAlgorithm is now at 100% line coverage of its changed lines. Two branches turned out to be unreachable, in DecodePartyInfo and in the TryResolveKeyGeneration size fallback, and were removed. No defect was found.


Generated by Claude Code

This branch was successfully deployed

1 active (outdated) deployment
code-review — 0af7f020 Deployed Nov 3, 2026 by polewskm via review #433
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant