-
-
Notifications
You must be signed in to change notification settings - Fork 324
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added JWS General JSON Serialization signing
resolves #129
- Loading branch information
Showing
6 changed files
with
354 additions
and
81 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
/* eslint-disable max-classes-per-file */ | ||
import FlattenedSign from '../flattened/sign.js' | ||
import { JWSInvalid } from '../../util/errors.js' | ||
|
||
import type { KeyLike, GeneralJWS, JWSHeaderParameters, SignOptions } from '../../types.d' | ||
|
||
export interface Signature { | ||
/** | ||
* Sets the JWS Protected Header on the Signature object. | ||
* | ||
* @param protectedHeader JWS Protected Header. | ||
*/ | ||
setProtectedHeader(protectedHeader: JWSHeaderParameters): Signature | ||
|
||
/** | ||
* Sets the JWS Unprotected Header on the Signature object. | ||
* | ||
* @param unprotectedHeader JWS Unprotected Header. | ||
*/ | ||
setUnprotectedHeader(unprotectedHeader: JWSHeaderParameters): Signature | ||
} | ||
|
||
interface SignatureReference { | ||
protectedHeader?: JWSHeaderParameters | ||
unprotectedHeader?: JWSHeaderParameters | ||
options?: SignOptions | ||
key: KeyLike | ||
} | ||
|
||
const signatureRef: WeakMap<IndividualSignature, SignatureReference> = new WeakMap() | ||
|
||
class IndividualSignature implements Signature { | ||
setProtectedHeader(protectedHeader: JWSHeaderParameters) { | ||
if (this._protectedHeader) { | ||
throw new TypeError('setProtectedHeader can only be called once') | ||
} | ||
this._protectedHeader = protectedHeader | ||
return this | ||
} | ||
|
||
setUnprotectedHeader(unprotectedHeader: JWSHeaderParameters) { | ||
if (this._unprotectedHeader) { | ||
throw new TypeError('setUnprotectedHeader can only be called once') | ||
} | ||
this._unprotectedHeader = unprotectedHeader | ||
return this | ||
} | ||
|
||
private set _protectedHeader(value: JWSHeaderParameters) { | ||
signatureRef.get(this)!.protectedHeader = value | ||
} | ||
|
||
private get _protectedHeader(): JWSHeaderParameters { | ||
return signatureRef.get(this)!.protectedHeader! | ||
} | ||
|
||
private set _unprotectedHeader(value: JWSHeaderParameters) { | ||
signatureRef.get(this)!.unprotectedHeader = value | ||
} | ||
|
||
private get _unprotectedHeader(): JWSHeaderParameters { | ||
return signatureRef.get(this)!.unprotectedHeader! | ||
} | ||
} | ||
|
||
/** | ||
* The GeneralSign class is a utility for creating General JWS objects. | ||
* | ||
* @example | ||
* ``` | ||
* // ESM import | ||
* import GeneralSign from 'jose/jws/general/sign' | ||
* ``` | ||
* | ||
* @example | ||
* ``` | ||
* // CJS import | ||
* const { default: GeneralSign } = require('jose/jws/general/sign') | ||
* ``` | ||
* | ||
* @example | ||
* ``` | ||
* // usage | ||
* import parseJwk from 'jose/jwk/parse' | ||
* | ||
* const encoder = new TextEncoder() | ||
* const ecPrivateKey = await parseJwk({ | ||
* alg: 'ES256', | ||
* crv: 'P-256', | ||
* kty: 'EC', | ||
* d: 'VhsfgSRKcvHCGpLyygMbO_YpXc7bVKwi12KQTE4yOR4', | ||
* x: 'ySK38C1jBdLwDsNWKzzBHqKYEE5Cgv-qjWvorUXk9fw', | ||
* y: '_LeQBw07cf5t57Iavn4j-BqJsAD1dpoz8gokd3sBsOo' | ||
* }) | ||
* const rsaPrivateKey = await parseJwk({ | ||
* alg: 'PS256', | ||
* e: 'AQAB', | ||
* n: 'vwKaDQxZtTDZVzoKmpzNNh5lG-tweqG_52qBRihiHPDqbvEDdM0GMFH6QVC7BZb_7lXvQ1QSYL8CWkigMuebx6LoCwCazsQ_IpaOYfmjkEAQ8HmXaRM5LyZ8Nch8iajgMSZkOTGi-15kLBskaM2VhC4l8WSVykgLqI196N1pd969xIXgweBqH1DJnLJoz5395j2b9SFRdu0VXIWzFtGA4DQmastcRvF-3McTebWTnTWVpEQPu-GixYbDyAtyjnVW7e2nfV0xoShYbFqWSJ4XwkbG7y8_mjsRL140LMHmq9mGR1WvF-KeT59iy5gg-63LXnUcTyAg45bMjH-ZRtlZ5Q', | ||
* d: 'M4srMPw1NPzSmYQzGlfX1JPNKwSUnLMLSxJlgh4ho7erO3bUPO-ajO2CP5_eZ_rAY3tTDnMgZnUE2IIioLn5Qp0GSvnFzKgOdXH1SCEKb0GqkInvPs6OLtgOyqCoYqlsnjbC8uAfH__vvisw3wsjHsEpQgOnnCdm5fwQjwc4j7zWby-EY0xFS8rUnfU6hSJ9Uw73ztftZuTXbmLFc5bw2mnAEuX18R9GVqduxRIqZQZfgUpmE5MbL8YBee3pZ51zjAa98z42kGS_0A33kXlXzcFDMd21cnfpZGKtIrigmTKabnc6MFZxaotmTaJqYUK58angQ3MkjTqMuj81JKRW4Q', | ||
* p: '-hAX8Gm_6xyIfo3v2pj6k9iaf7S8_vf8Hh05BrTpYHBb0FvnPxsm9vEd4H4BOKBQjoT5biXZpgCdTyLxvo1USrf0ocs3BHfBraSG_ohMlpjaR_biALz8tKOdlsAZIoUBwFMigdDfeOCnBtGao2UcyiYVPw4p5Nd-T36xRV7fMB0', | ||
* q: 'w4uUhbhWGt4-ibmwf2Kdaiz1PxyCxnZICBCGreH3WTvayjrAYEr_TVbMOU0_Bj7KagJxcwTEN2HlfFjLUDAatH8gwgmUPJzh5PmgJvOQJpKLVGGKU-xwDt2nbzZ3W0do8HtoC-rlL3cX9itmOI8YcxCRv4B1zrj5we53pH5itmk', | ||
* dp: 'TLxJjFX3Nd_Qpv1JYExXgK0UZCIDaT6SGG-hQ0Sa5SQ1mI_LO5tKbrb5Ex23pDfV4JY_sKRe0MkZfOJdSrs15aPjpw6kOHPDdFSrtEoBLqmDOlgxbEaSSaB3yH30eJpWOj2ItktxeDeAKeCCUqfBmOrs1Ce1hWr3cM-Q-JevZ6U', | ||
* dq: 'XIE8aqHQgfdfCFJCr5BcPW01O3zmVLKB0ubWf42lMJ6DGyX9-c-gxNpp1DW5ud-ca9fqCWpY1IZIRLHQxIdtKrP1MDXN3Xqt1l9MpwCT0duDdBCMmrUAMdgjrBXNEu5OM219xB2D_BdPy5GuUtVG0LAm8rv3fyq8ZETGbpenZPk', | ||
* qi: 'fsRY6JkXC2exI5Df16QnHO85T7QXAOX4SQbyF2jbiIqzyTrmIoMgxPeoFv26JqWjzaWtogIPLleFNA1EWpvtKwZQ8K0iCJZWoyCjYXUwhln1gaXjSRkecLL5_BUab8OmxmEChwDO95xyXd2r70ObaxqtLgpVqNERa-P2RArwMGQ', | ||
* kty: 'RSA', | ||
* }) | ||
* | ||
* const sign = new GeneralSign(encoder.encode('It’s a dangerous business, Frodo, going out your door.')) | ||
* | ||
* sign | ||
* .addSignature(ecPrivateKey) | ||
* .setProtectedHeader({ alg: 'ES256' }) | ||
* | ||
* sign | ||
* .addSignature(rsaPrivateKey) | ||
* .setProtectedHeader({ alg: 'PS256' }) | ||
* | ||
* const jws = await sign.sign() | ||
* ``` | ||
*/ | ||
export default class GeneralSign { | ||
private _payload: Uint8Array | ||
|
||
private _signatures: IndividualSignature[] = [] | ||
|
||
/** | ||
* @param payload Binary representation of the payload to sign. | ||
*/ | ||
constructor(payload: Uint8Array) { | ||
this._payload = payload | ||
} | ||
|
||
addSignature(key: KeyLike, options?: SignOptions): Signature { | ||
const signature = new IndividualSignature() | ||
signatureRef.set(signature, { key, options }) | ||
this._signatures.push(signature) | ||
return signature | ||
} | ||
|
||
/** | ||
* Signs and resolves the value of the General JWS object. | ||
*/ | ||
async sign(): Promise<GeneralJWS> { | ||
const jws: GeneralJWS = { | ||
signatures: [], | ||
} | ||
|
||
await Promise.all( | ||
this._signatures.map(async (sig, i) => { | ||
const { protectedHeader, unprotectedHeader, options, key } = signatureRef.get(sig)! | ||
const flattened = new FlattenedSign(this._payload) | ||
|
||
if (protectedHeader) { | ||
flattened.setProtectedHeader(protectedHeader) | ||
} | ||
|
||
if (unprotectedHeader) { | ||
flattened.setUnprotectedHeader(unprotectedHeader) | ||
} | ||
|
||
const { payload, ...rest } = await flattened.sign(key, options) | ||
|
||
if ('payload' in jws && jws.payload !== payload) { | ||
throw new JWSInvalid(`index ${i} signature produced a different payload`) | ||
} else { | ||
jws.payload = payload | ||
} | ||
|
||
jws.signatures.push(rest) | ||
}), | ||
) | ||
|
||
if ('payload' in jws && jws.payload === undefined) { | ||
delete jws.payload | ||
} | ||
|
||
return jws | ||
} | ||
} | ||
|
||
export type { KeyLike, GeneralJWS, JWSHeaderParameters } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* eslint-disable no-param-reassign */ | ||
import test from 'ava'; | ||
|
||
const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto'; | ||
Promise.all([ | ||
import(`${root}/jws/general/sign`), | ||
import(`${root}/jws/general/verify`), | ||
import(`${root}/util/random`), | ||
]).then( | ||
([{ default: GeneralSign }, { default: generalVerify }, { default: random }]) => { | ||
test.before(async (t) => { | ||
const encode = TextEncoder.prototype.encode.bind(new TextEncoder()); | ||
t.context.plaintext = encode('It’s a dangerous business, Frodo, going out your door.'); | ||
t.context.secret = random(new Uint8Array(48)); | ||
}); | ||
|
||
test('General JWS signing', async (t) => { | ||
const sig = new GeneralSign(t.context.plaintext); | ||
|
||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz' }) | ||
.setUnprotectedHeader({ alg: 'HS256' }); | ||
|
||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz' }) | ||
.setUnprotectedHeader({ alg: 'HS384' }); | ||
|
||
const generalJws = await sig.sign(); | ||
|
||
t.is( | ||
generalJws.payload, | ||
'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4', | ||
); | ||
t.is(generalJws.signatures.length, 2); | ||
}); | ||
|
||
test('General JWS signing b64:false', async (t) => { | ||
const sig = new GeneralSign(t.context.plaintext); | ||
|
||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz', b64: false, crit: ['b64'] }) | ||
.setUnprotectedHeader({ alg: 'HS256' }); | ||
|
||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz', b64: false, crit: ['b64'] }) | ||
.setUnprotectedHeader({ alg: 'HS384' }); | ||
|
||
const generalJws = await sig.sign(); | ||
|
||
t.false('payload' in generalJws); | ||
t.is(generalJws.signatures.length, 2); | ||
}); | ||
|
||
test('General JWS signing validations', async (t) => { | ||
const sig = new GeneralSign(t.context.plaintext); | ||
|
||
t.throws( | ||
() => { | ||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz', crit: ['b64'], b64: false, alg: 'HS256' }) | ||
.setProtectedHeader({ bar: 'baz', crit: ['b64'], b64: false, alg: 'HS256' }); | ||
}, | ||
{ instanceOf: TypeError, message: 'setProtectedHeader can only be called once' }, | ||
); | ||
|
||
t.throws( | ||
() => { | ||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz', crit: ['b64'], b64: true, alg: 'HS384' }) | ||
.setUnprotectedHeader({ foo: 'bar' }) | ||
.setUnprotectedHeader({ foo: 'bar' }); | ||
}, | ||
{ instanceOf: TypeError, message: 'setUnprotectedHeader can only be called once' }, | ||
); | ||
|
||
await t.throwsAsync(sig.sign(), { | ||
message: 'index 1 signature produced a different payload', | ||
code: 'ERR_JWS_INVALID', | ||
}); | ||
}); | ||
|
||
test('General JWS verify format validation', async (t) => { | ||
const sig = new GeneralSign(t.context.plaintext); | ||
|
||
sig | ||
.addSignature(t.context.secret) | ||
.setProtectedHeader({ bar: 'baz' }) | ||
.setUnprotectedHeader({ alg: 'HS256' }); | ||
|
||
const generalJws = await sig.sign(); | ||
|
||
{ | ||
await t.throwsAsync(generalVerify(null, t.context.secret), { | ||
message: 'General JWS must be an object', | ||
code: 'ERR_JWS_INVALID', | ||
}); | ||
} | ||
|
||
{ | ||
await t.throwsAsync(generalVerify({ signatures: null }, t.context.secret), { | ||
message: 'JWS Signatures missing or incorrect type', | ||
code: 'ERR_JWS_INVALID', | ||
}); | ||
} | ||
|
||
{ | ||
await t.throwsAsync(generalVerify({ signatures: [null] }, t.context.secret), { | ||
message: 'JWS Signatures missing or incorrect type', | ||
code: 'ERR_JWS_INVALID', | ||
}); | ||
} | ||
|
||
{ | ||
const jws = { payload: generalJws.payload, signatures: [] }; | ||
|
||
await t.throwsAsync(generalVerify(jws, t.context.secret), { | ||
message: 'signature verification failed', | ||
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED', | ||
}); | ||
} | ||
|
||
{ | ||
await t.notThrowsAsync(generalVerify(generalJws, t.context.secret)); | ||
} | ||
|
||
{ | ||
const { payload, signatures } = generalJws; | ||
const jws = { payload, signatures: [...signatures, {}] }; | ||
|
||
await t.notThrowsAsync(generalVerify(jws, t.context.secret)); | ||
} | ||
|
||
{ | ||
const { payload, signatures } = generalJws; | ||
const jws = { payload, signatures: [{}, ...signatures] }; | ||
|
||
await t.notThrowsAsync(generalVerify(jws, t.context.secret)); | ||
} | ||
}); | ||
}, | ||
(err) => { | ||
test('failed to import', (t) => { | ||
console.error(err); | ||
t.fail(); | ||
}); | ||
}, | ||
); |
Oops, something went wrong.