Skip to content

Commit

Permalink
feat: support recognizing proprietary crit header parameters
Browse files Browse the repository at this point in the history
closes #123
  • Loading branch information
panva committed Dec 6, 2020
1 parent dade1fd commit 5163116
Show file tree
Hide file tree
Showing 11 changed files with 157 additions and 29 deletions.
4 changes: 2 additions & 2 deletions src/jwe/flattened/decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import validateCrit from '../../lib/validate_crit.js'
import validateAlgorithms from '../../lib/validate_algorithms.js'

const generateCek = cekFactory(random)
const checkCrit = validateCrit.bind(undefined, JWEInvalid, new Map())
const checkExtensions = validateCrit.bind(undefined, JWEInvalid, new Map())
const checkAlgOption = validateAlgorithms.bind(undefined, 'keyManagementAlgorithms')
const checkEncOption = validateAlgorithms.bind(undefined, 'contentEncryptionAlgorithms')

Expand Down Expand Up @@ -151,7 +151,7 @@ export default async function flattenedDecrypt(
...jwe.unprotected,
}

checkCrit(parsedProt, joseHeader)
checkExtensions(options?.crit, parsedProt, joseHeader)

if (joseHeader.zip !== undefined) {
if (!parsedProt || !parsedProt.zip) {
Expand Down
4 changes: 2 additions & 2 deletions src/jwe/flattened/encrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { encoder, decoder, concat } from '../../lib/buffer_utils.js'
import validateCrit from '../../lib/validate_crit.js'

const generateIv = ivFactory(random)
const checkCrit = validateCrit.bind(undefined, JWEInvalid, new Map())
const checkExtensions = validateCrit.bind(undefined, JWEInvalid, new Map())

/**
* The FlattenedEncrypt class is a utility for creating Flattened JWE
Expand Down Expand Up @@ -205,7 +205,7 @@ export default class FlattenedEncrypt {
...this._sharedUnprotectedHeader,
}

checkCrit(this._protectedHeader, joseHeader)
checkExtensions(options?.crit, this._protectedHeader, joseHeader)

if (joseHeader.zip !== undefined) {
if (!this._protectedHeader || !this._protectedHeader.zip) {
Expand Down
7 changes: 4 additions & 3 deletions src/jws/compact/sign.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable no-underscore-dangle */

import FlattenedSign from '../flattened/sign.js'
import type { JWSHeaderParameters, KeyLike } from '../../types.d'
import type { JWSHeaderParameters, KeyLike, SignOptions } from '../../types.d'

/**
* The CompactSign class is a utility for creating Compact JWS strings.
Expand Down Expand Up @@ -64,9 +64,10 @@ export default class CompactSign {
* Signs and resolves the value of the Compact JWS string.
*
* @param key Private Key or Secret to sign the JWS with.
* @param options JWS Sign options.
*/
async sign(key: KeyLike): Promise<string> {
const jws = await this._flattened.sign(key)
async sign(key: KeyLike, options?: SignOptions): Promise<string> {
const jws = await this._flattened.sign(key, options)

if (jws.payload === undefined) {
throw new TypeError('use the flattened module for creating JWS with b64: false')
Expand Down
7 changes: 4 additions & 3 deletions src/jws/flattened/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { encoder, decoder, concat } from '../../lib/buffer_utils.js'

import { encode as base64url } from '../../runtime/base64url.js'
import sign from '../../runtime/sign.js'
import type { KeyLike, FlattenedJWS, JWSHeaderParameters } from '../../types.d'
import type { KeyLike, FlattenedJWS, JWSHeaderParameters, SignOptions } from '../../types.d'
import checkKeyType from '../../lib/check_key_type.js'
import validateCrit from '../../lib/validate_crit.js'

Expand Down Expand Up @@ -92,8 +92,9 @@ export default class FlattenedSign {
* Signs and resolves the value of the Flattened JWS object.
*
* @param key Private Key or Secret to sign the JWS with.
* @param options JWS Sign options.
*/
async sign(key: KeyLike): Promise<FlattenedJWS> {
async sign(key: KeyLike, options?: SignOptions): Promise<FlattenedJWS> {
if (!this._protectedHeader && !this._unprotectedHeader) {
throw new JWSInvalid(
'either setProtectedHeader or setUnprotectedHeader must be called before #sign()',
Expand All @@ -111,7 +112,7 @@ export default class FlattenedSign {
...this._unprotectedHeader,
}

const extensions = checkExtensions(this._protectedHeader, joseHeader)
const extensions = checkExtensions(options?.crit, this._protectedHeader, joseHeader)

let b64: boolean = true
if (extensions.has('b64')) {
Expand Down
2 changes: 1 addition & 1 deletion src/jws/flattened/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export default async function flattenedVerify(
...jws.header,
}

const extensions = checkExtensions(parsedProt, joseHeader)
const extensions = checkExtensions(options?.crit, parsedProt, joseHeader)

let b64: boolean = true
if (extensions.has('b64')) {
Expand Down
7 changes: 4 additions & 3 deletions src/jwt/sign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import CompactSign from '../jws/compact/sign.js'
import { JWTInvalid } from '../util/errors.js'
import type { JWSHeaderParameters, JWTPayload, KeyLike } from '../types.d'
import type { JWSHeaderParameters, JWTPayload, KeyLike, SignOptions } from '../types.d'
import { encoder } from '../lib/buffer_utils.js'
import ProduceJWT from '../lib/jwt_producer.js'

Expand Down Expand Up @@ -63,14 +63,15 @@ export default class SignJWT extends ProduceJWT {
* Signs and returns the JWT.
*
* @param key Private Key or Secret to sign the JWT with.
* @param options JWT Sign options.
*/
async sign(key: KeyLike): Promise<string> {
async sign(key: KeyLike, options?: SignOptions): Promise<string> {
const sig = new CompactSign(encoder.encode(JSON.stringify(this._payload)))
sig.setProtectedHeader(this._protectedHeader)
if (this._protectedHeader.crit?.includes('b64') && this._protectedHeader.b64 === false) {
throw new JWTInvalid('JWTs MUST NOT use unencoded payload')
}
return sig.sign(key)
return sig.sign(key, options)
}
}

Expand Down
18 changes: 12 additions & 6 deletions src/lib/validate_crit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ interface CritCheckHeader {

function validateCrit(
Err: typeof JWEInvalid | typeof JWSInvalid,
supported: Map<string, boolean>,
recognizedDefault: Map<string, boolean>,
recognizedOption: { [propName: string]: boolean } | undefined,
protectedHeader: CritCheckHeader,
joseHeader: CritCheckHeader,
) {
Expand All @@ -30,17 +31,22 @@ function validateCrit(
)
}

let recognized: Map<string, boolean>
if (recognizedOption !== undefined) {
recognized = new Map([...Object.entries(recognizedOption), ...recognizedDefault.entries()])
} else {
recognized = recognizedDefault
}

// eslint-disable-next-line no-restricted-syntax
for (const parameter of protectedHeader.crit) {
if (!supported.has(parameter)) {
throw new JOSENotSupported(
`Extension Header Parameter "${parameter}" is not supported by this implementation`,
)
if (!recognized.has(parameter)) {
throw new JOSENotSupported(`Extension Header Parameter "${parameter}" is not recognized`)
}

if (joseHeader[parameter] === undefined) {
throw new Err(`Extension Header Parameter "${parameter}" is missing`)
} else if (supported.get(parameter) && protectedHeader[parameter] === undefined) {
} else if (recognized.get(parameter) && protectedHeader[parameter] === undefined) {
throw new Err(`Extension Header Parameter "${parameter}" MUST be integrity protected`)
}
}
Expand Down
40 changes: 37 additions & 3 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,39 @@ export interface JWEHeaderParameters extends JoseHeaderParameters {
[propName: string]: any
}

/**
* Shared Interface with a "crit" property for all sign and verify operations.
*/
export interface CritOption {
/**
* An object with keys representing recognized "crit" (Critical) Header Parameter
* names. The value for those is either `true` or `false`. `true` when the
* Header Parameter MUST be integrity protected, `false` when it's irrelevant.
*
* This makes the "Extension Header Parameter "${parameter}" is not recognized"
* error go away.
*
* Use this when a given JWS/JWT/JWE profile requires the use of proprietary
* non-registered "crit" (Critical) Header Parameters. This will only make sure
* the Header Parameter is syntactically correct when provided and that it is
* optionally integrity protected. It will not process the Header Parameter in
* any way or reject if the operation if it is missing. You MUST still
* verify the Header Parameter was present and process it according to the
* profile's validation steps after the operation succeeds.
*
* The JWS extension Header Parameter `b64` is always recognized and processed
* properly. No other registered Header Parameters that need this kind of
* default built-in treatment are currently available.
*/
crit?: {
[propName: string]: boolean
}
}

/**
* JWE Decryption options.
*/
export interface DecryptOptions {
export interface DecryptOptions extends CritOption {
/**
* A list of accepted JWE "alg" (Algorithm) Header Parameter values.
*/
Expand All @@ -337,7 +366,7 @@ export interface DecryptOptions {
/**
* JWE Encryption options.
*/
export interface EncryptOptions {
export interface EncryptOptions extends CritOption {
/**
* In a browser runtime you have to provide an implementation for Deflate Raw
* when you will be producing JWEs with compressed plaintext.
Expand Down Expand Up @@ -390,13 +419,18 @@ export interface JWTClaimVerificationOptions {
/**
* JWS Verification options.
*/
export interface VerifyOptions {
export interface VerifyOptions extends CritOption {
/**
* A list of accepted JWS "alg" (Algorithm) Header Parameter values.
*/
algorithms?: string[]
}

/**
* JWS Signing options.
*/
export interface SignOptions extends CritOption {}

/**
* Recognized JWT Claims Set members, any other members
* may also be present.
Expand Down
3 changes: 1 addition & 2 deletions test/jws/crit.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ Promise.all([import(`${root}/jws/flattened/sign`), import(`${root}/jws/flattened
.sign(new Uint8Array(32)),
{
code: 'ERR_JOSE_NOT_SUPPORTED',
message:
'Extension Header Parameter "unsupported" is not supported by this implementation',
message: 'Extension Header Parameter "unsupported" is not recognized',
},
);
});
Expand Down
49 changes: 47 additions & 2 deletions test/jwt/encrypt.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import test from 'ava';
import timekeeper from 'timekeeper';

const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto';
Promise.all([import(`${root}/jwt/encrypt`), import(`${root}/jwe/compact/decrypt`)]).then(
([{ default: EncryptJWT }, { default: compactDecrypt }]) => {
Promise.all([
import(`${root}/jwt/encrypt`),
import(`${root}/jwe/compact/decrypt`),
import(`${root}/jwt/decrypt`),
]).then(
([{ default: EncryptJWT }, { default: compactDecrypt }, { default: jwtDecrypt }]) => {
const now = 1604416038;

test.before(async (t) => {
Expand All @@ -28,6 +32,47 @@ Promise.all([import(`${root}/jwt/encrypt`), import(`${root}/jwe/compact/decrypt`
);
});

test('EncryptJWT w/crit', async (t) => {
const expected =
'eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwiY3JpdCI6WyJodHRwOi8vb3BlbmJhbmtpbmcub3JnLnVrL2lhdCJdLCJodHRwOi8vb3BlbmJhbmtpbmcub3JnLnVrL2lhdCI6MH0..AAAAAAAAAAAAAAAA.eKqvvA6MxuqSRbLVFIidFJb8x4lzPytWkoA.Kl-auiUImwUWk4X0xpxa8A';
await t.throwsAsync(
new EncryptJWT(t.context.payload)
.setInitializationVector(t.context.initializationVector)
.setProtectedHeader({
alg: 'dir',
enc: 'A128GCM',
crit: ['http:https://openbanking.org.uk/iat'],
'http:https://openbanking.org.uk/iat': 0,
})
.encrypt(t.context.secret),
{
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http:https://openbanking.org.uk/iat" is not recognized',
},
);

await t.notThrowsAsync(async () => {
const jwt = await new EncryptJWT(t.context.payload)
.setInitializationVector(t.context.initializationVector)
.setProtectedHeader({
alg: 'dir',
enc: 'A128GCM',
crit: ['http:https://openbanking.org.uk/iat'],
'http:https://openbanking.org.uk/iat': 0,
})
.encrypt(t.context.secret, { crit: { 'http:https://openbanking.org.uk/iat': true } });
t.is(jwt, expected);
});

await t.throwsAsync(jwtDecrypt(expected, t.context.secret), {
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http:https://openbanking.org.uk/iat" is not recognized',
});
await t.notThrowsAsync(
jwtDecrypt(expected, t.context.secret, { crit: { 'http:https://openbanking.org.uk/iat': true } }),
);
});

test('new EncryptJWT', (t) => {
t.throws(() => new EncryptJWT(), {
instanceOf: TypeError,
Expand Down
45 changes: 43 additions & 2 deletions test/jwt/sign.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import test from 'ava';
import timekeeper from 'timekeeper';

const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto';
Promise.all([import(`${root}/jwt/sign`), import(`${root}/jws/compact/verify`)]).then(
([{ default: SignJWT }, { default: compactVerify }]) => {
Promise.all([
import(`${root}/jwt/sign`),
import(`${root}/jws/compact/verify`),
import(`${root}/jwt/verify`),
]).then(
([{ default: SignJWT }, { default: compactVerify }, { default: jwtVerify }]) => {
const now = 1604416038;

test.before(async (t) => {
Expand All @@ -26,6 +30,43 @@ Promise.all([import(`${root}/jwt/sign`), import(`${root}/jws/compact/verify`)]).
);
});

test('SignJWT w/crit', async (t) => {
const expected =
'eyJhbGciOiJIUzI1NiIsImNyaXQiOlsiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay9pYXQiXSwiaHR0cDovL29wZW5iYW5raW5nLm9yZy51ay9pYXQiOjB9.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZX0.YzOrPZaNql7PpCo43HAJdj-LASP8lOmtb-Bzj9OrNAk';
await t.throwsAsync(
new SignJWT(t.context.payload)
.setProtectedHeader({
alg: 'HS256',
crit: ['http:https://openbanking.org.uk/iat'],
'http:https://openbanking.org.uk/iat': 0,
})
.sign(t.context.secret),
{
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http:https://openbanking.org.uk/iat" is not recognized',
},
);

await t.notThrowsAsync(async () => {
const jwt = await new SignJWT(t.context.payload)
.setProtectedHeader({
alg: 'HS256',
crit: ['http:https://openbanking.org.uk/iat'],
'http:https://openbanking.org.uk/iat': 0,
})
.sign(t.context.secret, { crit: { 'http:https://openbanking.org.uk/iat': true } });
t.is(jwt, expected);
});

await t.throwsAsync(jwtVerify(expected, t.context.secret), {
code: 'ERR_JOSE_NOT_SUPPORTED',
message: 'Extension Header Parameter "http:https://openbanking.org.uk/iat" is not recognized',
});
await t.notThrowsAsync(
jwtVerify(expected, t.context.secret, { crit: { 'http:https://openbanking.org.uk/iat': true } }),
);
});

test('new SignJWT', (t) => {
t.throws(() => new SignJWT(), {
instanceOf: TypeError,
Expand Down

0 comments on commit 5163116

Please sign in to comment.