Skip to content

fix(sdk): emit RFC 7518 raw ECDSA signatures in JWS (DSPX-3397) - #987

Draft
dmihalcik-virtru wants to merge 1 commit into
mainfrom
dspx-3397-1-jws-signatures
Draft

fix(sdk): emit RFC 7518 raw ECDSA signatures in JWS (DSPX-3397)#987
dmihalcik-virtru wants to merge 1 commit into
mainfrom
dspx-3397-1-jws-signatures

Conversation

@dmihalcik-virtru

Copy link
Copy Markdown
Member

Stack 1/8, split out of #939. Base: main.

What

cryptoService.sign() returns DER-encoded ECDSA signatures, but JWS requires raw IEEE P1363 (R || S) per RFC 7518 §3.4. Both JWS emitters were shipping DER:

  • tdf3/src/crypto/jwt.ts — the KAS rewrap request token
  • src/auth/dpop.ts — the DPoP proof

so any EC-keyed token was rejected by conformant verifiers. This is the "Invalid token signature" / "unable to verify request token" failure seen against Keycloak. verifyJwt had the mirror bug: it fed a raw JWS signature to a verifier that expects DER.

Separately, reqSignature defaulted to RS256 for the rewrap request token. WebCrypto rejects signing an EC private key with RSA params (Unable to use this key to sign), so an EC dpop key could not produce a rewrap token at all. The alg is now derived from the key's algorithm.

Why it wasn't caught

The SDK's own sign/verify pair is symmetric — both sides speak DER — so it round-trips internally even when the wire format is wrong. The mock test server only decodeJwts the rewrap token and never checks the signature. Both passed while the real platform failed.

The new tests verify against jose.jwtVerify, an independent RFC-conformant verifier (the same library Keycloak uses), which is what closes that gap.

Changes

  • tdf3/src/crypto/jwt.ts: convert to raw on sign, back to DER on verify, for ES*
  • src/auth/dpop.ts: same conversion in the proof signer
  • tdf3/src/tdf.ts: signingAlgForKeyAlgorithm() picks the rewrap token alg from the dpop key
  • tdf3/src/crypto/core/signing.ts: export ieeeP1363ToDer / derToIeeeP1363
  • tdf3/src/crypto/declarations.ts: add isAsymmetricSigningAlgorithm() so the JWS alg header is validated rather than blind-cast (the JWS alg space includes PS256/EdDSA, which we cannot sign with)

Tests

  • tests/mocha/dpop-proof.spec.ts — DPoP proofs (ES256/384/512, RS256) verified with jose.jwtVerify, plus tamper and wrong-key cases
  • tests/mocha/reqsignature-jws.spec.ts — rewrap request token, same treatment
  • tests/mocha/helpers/jws-keys.ts — shared WebCrypto→PEM→SDK key fixtures for both suites
  • tests/mocha/encrypt-decrypt.spec.ts — end-to-end decrypt with an EC dpop key

Follow-up

The conversion creates a round-trip: sign() encodes raw→DER, and both callers immediately decode DER→raw. Removing it (making sign()/verify() speak raw) is DSPX-3634 and is stack 8/8, kept separate because it changes the public CryptoService contract.

How to test

cd lib && npm test

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fa920d4a-51cc-4142-be34-187c0945ae48

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

cryptoService.sign() returns DER-encoded ECDSA signatures, but JWS requires
raw IEEE P1363 (R || S) per RFC 7518 section 3.4. Both JWS emitters -- the KAS
rewrap request token (tdf3/src/crypto/jwt.ts) and the DPoP proof signer
(src/auth/dpop.ts) -- were shipping DER, so any EC-keyed token was rejected by
conformant verifiers (Keycloak, panva-jose). verifyJwt had the mirror bug: it
fed a raw JWS signature to a verifier expecting DER.

Also fixes reqSignature defaulting to RS256 for the rewrap request token.
WebCrypto rejects signing an EC private key with RSA params ("Unable to use
this key to sign"), so an EC dpop key could not produce a rewrap token at all;
the alg is now derived from the key's algorithm.

Supporting changes:
- export ieeeP1363ToDer / derToIeeeP1363 for these callers
- add isAsymmetricSigningAlgorithm() so the JWS `alg` header is validated
  rather than blind-cast to the narrower set CryptoService can actually sign
  with (the JWS alg space includes PS256/EdDSA, which we do not support)

The round-trip this creates (sign encodes to DER, caller decodes back to raw)
is tracked for removal in DSPX-3634.

Signed-off-by: Dave Mihalcik <dmihalcik@virtru.com>
@dmihalcik-virtru
dmihalcik-virtru force-pushed the dspx-3397-1-jws-signatures branch from 756b4b3 to b220a91 Compare August 10, 2026 17:47
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant