Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions lib/src/auth/dpop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import type {
AsymmetricSigningAlgorithm,
KeyAlgorithm,
} from '../../tdf3/src/crypto/declarations.js';
import { isRsaKeyAlgorithm } from '../../tdf3/src/crypto/declarations.js';
import {
isAsymmetricSigningAlgorithm,
isRsaKeyAlgorithm,
} from '../../tdf3/src/crypto/declarations.js';

export type JsonObject = { [Key in string]?: JsonValue };
export type JsonArray = JsonValue[];
Expand All @@ -21,11 +24,11 @@ function buf(input: string): Uint8Array {
return encoder.encode(input);
}

interface DPoPJwtHeaderParameters {
type DPoPJwtHeaderParameters = {
alg: JWSAlgorithm;
typ: string;
typ: 'dpop+jwt';
jwk: JsonWebKey;
}
};

/**
* Minimal JWT sign() implementation using CryptoService.
Expand All @@ -37,11 +40,11 @@ async function jwt(
cryptoService: CryptoService
) {
const input = `${b64u(buf(JSON.stringify(header)))}.${b64u(buf(JSON.stringify(claimsSet)))}`;
const signature = await cryptoService.sign(
buf(input),
privateKey,
header.alg as AsymmetricSigningAlgorithm
);
const { alg } = header;
if (!isAsymmetricSigningAlgorithm(alg)) {
throw new UnsupportedOperationError(`unsupported DPoP alg: ${alg}`);
}
const signature = await cryptoService.sign(buf(input), privateKey, alg);
return `${input}.${b64u(signature)}`;
}

Expand Down Expand Up @@ -120,8 +123,10 @@ class UnsupportedOperationError extends Error {

/**
* Determines a supported JWS `alg` identifier from PublicKeyInfo algorithm string.
* Returns an AsymmetricSigningAlgorithm (the subset CryptoService can sign with);
* notably, it does not support PS256/EdDSA members of JWSAlgorithm.
*/
function determineJWSAlgorithmFromKeyInfo(algorithm: KeyAlgorithm): JWSAlgorithm {
function determineJWSAlgorithmFromKeyInfo(algorithm: KeyAlgorithm): AsymmetricSigningAlgorithm {
if (isRsaKeyAlgorithm(algorithm)) {
return 'RS256';
}
Expand Down
6 changes: 4 additions & 2 deletions lib/tdf3/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ async function sign(
try {
token = await signJwt(cryptoService, payload, signingMaterial, header);
} catch (error) {
throw new ConfigurationError(`Signing assertion failed: ${error.message}`, error);
const msg = error instanceof Error ? error.message : String(error);
throw new ConfigurationError(`Signing assertion failed: ${msg}`, error);
}
thiz.binding.method = 'jws';
thiz.binding.signature = token;
Expand Down Expand Up @@ -185,7 +186,8 @@ export async function verify(
});
payload = result.payload as AssertionPayload;
} catch (error) {
throw new InvalidFileError(`Verifying assertion failed: ${error.message}`, error);
const msg = error instanceof Error ? error.message : String(error);
throw new InvalidFileError(`Verifying assertion failed: ${msg}`, error);
}
const { assertionHash, assertionSig } = payload;

Expand Down
153 changes: 10 additions & 143 deletions lib/tdf3/src/crypto/core/signing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,141 +39,13 @@ function getSigningAlgorithmParams(algorithm: AsymmetricSigningAlgorithm): {
}
}

/**
* Convert IEEE P1363 signature format (used by WebCrypto ECDSA) to DER format (used by JWT).
* RS256 signatures don't need conversion.
*/
function ieeeP1363ToDer(signature: Uint8Array, algorithm: AsymmetricSigningAlgorithm): Uint8Array {
if (algorithm === 'RS256') {
return signature;
}

// IEEE P1363: r || s where each is padded to key size
const halfLen = signature.length / 2;
const r = signature.slice(0, halfLen);
const s = signature.slice(halfLen);

// Remove leading zeros but keep one if the high bit is set
const trimLeadingZeros = (arr: Uint8Array): Uint8Array => {
let i = 0;
while (i < arr.length - 1 && arr[i] === 0) i++;
return arr.slice(i);
};

let rTrimmed = trimLeadingZeros(r);
let sTrimmed = trimLeadingZeros(s);

// Add leading zero if high bit is set (to keep positive in DER)
if (rTrimmed[0] & 0x80) {
const padded = new Uint8Array(rTrimmed.length + 1);
padded.set(rTrimmed, 1);
rTrimmed = padded;
}
if (sTrimmed[0] & 0x80) {
const padded = new Uint8Array(sTrimmed.length + 1);
padded.set(sTrimmed, 1);
sTrimmed = padded;
}

// DER SEQUENCE: 0x30 [length] [r INTEGER] [s INTEGER]
// INTEGER: 0x02 [length] [value]
const rDer = new Uint8Array([0x02, rTrimmed.length, ...rTrimmed]);
const sDer = new Uint8Array([0x02, sTrimmed.length, ...sTrimmed]);

const seqLen = rDer.length + sDer.length;
// DER length: short-form for < 128, long-form (0x81 nn) for 128-255.
// ECDSA sequences never exceed 255 bytes for any supported curve.
const lenBytes = seqLen < 128 ? new Uint8Array([seqLen]) : new Uint8Array([0x81, seqLen]);
const result = new Uint8Array(1 + lenBytes.length + seqLen);
result[0] = 0x30;
result.set(lenBytes, 1);
result.set(rDer, 1 + lenBytes.length);
result.set(sDer, 1 + lenBytes.length + rDer.length);

return result;
}

/**
* Convert DER signature format (used by JWT) to IEEE P1363 format (used by WebCrypto ECDSA).
* RS256 signatures don't need conversion.
*/
function derToIeeeP1363(signature: Uint8Array, algorithm: AsymmetricSigningAlgorithm): Uint8Array {
if (algorithm === 'RS256') {
return signature;
}

// Determine the expected component length based on algorithm
let componentLen: number;
switch (algorithm) {
case 'ES256':
componentLen = 32;
break;
case 'ES384':
componentLen = 48;
break;
case 'ES512':
componentLen = 66;
break;
default:
throw new ConfigurationError(`Unsupported algorithm for DER conversion: ${algorithm}`);
}

if (signature[0] !== 0x30) {
throw new ConfigurationError('Invalid DER signature: expected SEQUENCE');
}

// Skip SEQUENCE tag, then parse DER length (short- or long-form).
let offset = 1;
if (signature[offset] & 0x80) {
// Long-form: low 7 bits = number of subsequent length bytes.
const lenBytesCount = signature[offset] & 0x7f;
if (lenBytesCount === 0 || lenBytesCount > 4) {
throw new ConfigurationError('Invalid DER signature: invalid long-form length');
}
offset += 1 + lenBytesCount;
if (offset > signature.length) {
throw new ConfigurationError('Invalid DER signature: length bytes exceed signature length');
}
} else {
// Short-form: single length byte.
offset += 1;
}

// Parse r INTEGER
if (signature[offset] !== 0x02) {
throw new ConfigurationError('Invalid DER signature: expected INTEGER for r');
}
const rLen = signature[offset + 1];
offset += 2;
let r = signature.slice(offset, offset + rLen);
offset += rLen;

// Parse s INTEGER
if (signature[offset] !== 0x02) {
throw new ConfigurationError('Invalid DER signature: expected INTEGER for s');
}
const sLen = signature[offset + 1];
offset += 2;
let s = signature.slice(offset, offset + sLen);

// Remove leading zero padding if present
if (r[0] === 0 && r.length > componentLen) {
r = r.slice(1);
}
if (s[0] === 0 && s.length > componentLen) {
s = s.slice(1);
}

// Pad to component length
const result = new Uint8Array(componentLen * 2);
result.set(r, componentLen - r.length);
result.set(s, componentLen * 2 - s.length);

return result;
}

/**
* Sign data with an asymmetric private key.
*
* ECDSA signatures come back as raw IEEE P1363 (`R || S`) — the fixed-width
* encoding WebCrypto emits and the one RFC 7518 section 3.4 requires on the JWS
* wire — so nothing transcodes between here and the token. RSA signatures have
* a single encoding.
*/
export async function sign(
data: Uint8Array,
Expand All @@ -185,15 +57,14 @@ export async function sign(
// Unwrap the internal CryptoKey
const key = unwrapKey(privateKey);

// Sign the data
const signature = await crypto.subtle.sign(signParams, key, data);

// Convert from IEEE P1363 to DER for EC algorithms
return ieeeP1363ToDer(new Uint8Array(signature), algorithm);
return new Uint8Array(await crypto.subtle.sign(signParams, key, data));
}

/**
* Verify signature with an asymmetric public key.
*
* Expects the encoding {@link sign} produces: raw IEEE P1363 for ECDSA. A
* wrong-length signature fails verification rather than throwing.
*/
export async function verify(
data: Uint8Array,
Expand All @@ -206,9 +77,5 @@ export async function verify(
// Unwrap the internal CryptoKey
const key = unwrapKey(publicKey);

// Convert from DER to IEEE P1363 for EC algorithms
const ieeeSignature = derToIeeeP1363(signature, algorithm);

// Verify the signature
return crypto.subtle.verify(signParams, key, ieeeSignature, data);
return crypto.subtle.verify(signParams, key, signature, data);
}
31 changes: 30 additions & 1 deletion lib/tdf3/src/crypto/declarations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,10 +185,32 @@ export type SymmetricKey = {
*/
export type ECCurve = 'P-256' | 'P-384' | 'P-521';

/**
* ECDSA signing algorithms. Signatures for these are raw IEEE P1363 (`R || S`)
* everywhere in this SDK, per RFC 7518 §3.4; see `crypto/core/signing.ts`.
*/
export const EC_SIGNING_ALGORITHMS = ['ES256', 'ES384', 'ES512'] as const;

export type EcSigningAlgorithm = (typeof EC_SIGNING_ALGORITHMS)[number];

/**
* Runtime list of asymmetric signing algorithms. Used to validate
* untyped/JWS-header algorithm strings.
*/
export const ASYMMETRIC_SIGNING_ALGORITHMS = ['RS256', ...EC_SIGNING_ALGORITHMS] as const;

/**
* Asymmetric signing algorithms (require PEM keys).
*/
export type AsymmetricSigningAlgorithm = 'RS256' | 'ES256' | 'ES384' | 'ES512';
export type AsymmetricSigningAlgorithm = (typeof ASYMMETRIC_SIGNING_ALGORITHMS)[number];

/**
* Type guard narrowing an arbitrary string to an algorithm CryptoService can
* sign/verify with.
*/
export function isAsymmetricSigningAlgorithm(alg: string): alg is AsymmetricSigningAlgorithm {
return (ASYMMETRIC_SIGNING_ALGORITHMS as readonly string[]).includes(alg);
}

/**
* Symmetric signing algorithm (requires raw key bytes).
Expand Down Expand Up @@ -287,6 +309,12 @@ export type CryptoService = {

/**
* Sign data with an asymmetric private key.
*
* ECDSA signature encoding: returns raw IEEE P1363 (`R || S`), fixed-width
* per curve — 64 bytes for ES256, 96 for ES384, 132 for ES512.
*
* RSA uses RSASSA-PKCS1-v1_5 with SHA-256.
*
* @param data - Data to sign
* @param privateKey - Opaque private key
* @param algorithm - Signing algorithm (RS256, ES256, ES384, ES512)
Expand All @@ -299,6 +327,7 @@ export type CryptoService = {

/**
* Verify signature with an asymmetric public key.
*
* @param data - Original data that was signed
* @param signature - Signature to verify
* @param publicKey - Opaque public key
Expand Down
22 changes: 10 additions & 12 deletions lib/tdf3/src/crypto/jwt.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
type AsymmetricSigningAlgorithm,
isAsymmetricSigningAlgorithm,
type CryptoService,
type PrivateKey,
type PublicKey,
Expand Down Expand Up @@ -134,11 +134,10 @@ export async function signJwt(
if (key._brand !== 'PrivateKey') {
throw new Error(`${header.alg} requires a PrivateKey`);
}
signature = await cryptoService.sign(
signingInputBytes,
key,
header.alg as AsymmetricSigningAlgorithm
);
if (!isAsymmetricSigningAlgorithm(header.alg)) {
throw new Error(`Unsupported JWS signing algorithm: ${header.alg}`);
}
signature = await cryptoService.sign(signingInputBytes, key, header.alg);
}

// Return compact JWT
Expand Down Expand Up @@ -232,12 +231,11 @@ export async function verifyJwt(
typeof key === 'string'
? await cryptoService.importPublicKey(key, { usage: 'sign' })
: (key as PublicKey);
valid = await cryptoService.verify(
signingInputBytes,
signature,
publicKey,
header.alg as AsymmetricSigningAlgorithm
);
if (!isAsymmetricSigningAlgorithm(header.alg)) {
throw new joseErrors.JWTInvalid(`Invalid JWT: unsupported algorithm "${header.alg}"`);
}
// Sigs are IEEE P1363 for ECDSA (RFC 7518 §3.4), PKCS#1 for RSA.
valid = await cryptoService.verify(signingInputBytes, signature, publicKey, header.alg);
}

if (!valid) {
Expand Down
Loading
Loading