Skip to content

Commit

Permalink
feat: added JWS General JSON Serialization verification
Browse files Browse the repository at this point in the history
resolves #129
  • Loading branch information
panva committed Dec 16, 2020
1 parent 40791da commit 55b7781
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 0 deletions.
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"embedded",
"encrypt",
"flattened",
"general",
"isomorphic",
"jose",
"json web token",
Expand Down Expand Up @@ -171,6 +172,15 @@
"import": "./dist/node/webcrypto/esm/jws/flattened/verify.js",
"require": "./dist/node/webcrypto/cjs/jws/flattened/verify.js"
},
"./jws/general/verify": {
"browser": "./dist/browser/jws/general/verify.js",
"import": "./dist/node/esm/jws/general/verify.js",
"require": "./dist/node/cjs/jws/general/verify.js"
},
"./webcrypto/jws/general/verify": {
"import": "./dist/node/webcrypto/esm/jws/general/verify.js",
"require": "./dist/node/webcrypto/cjs/jws/general/verify.js"
},
"./jwt/decrypt": {
"browser": "./dist/browser/jwt/decrypt.js",
"import": "./dist/node/esm/jwt/decrypt.js",
Expand Down
95 changes: 95 additions & 0 deletions src/jws/general/verify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import verify from '../flattened/verify.js'
import type {
GeneralJWSInput,
GeneralVerifyResult,
FlattenedJWSInput,
GetKeyFunction,
JWSHeaderParameters,
KeyLike,
VerifyOptions,
} from '../../types.d'
import { JWSInvalid, JWSSignatureVerificationFailed } from '../../util/errors.js'
import isObject from '../../lib/is_object.js'

/**
* Interface for General JWS Verification dynamic key resolution.
* No token components have been verified at the time of this function call.
*/
export interface GeneralVerifyGetKey
extends GetKeyFunction<JWSHeaderParameters, FlattenedJWSInput> {}

/**
* Verifies the signature and format of and afterwards decodes the General JWS.
*
* @param jws General JWS.
* @param key Key, or a function resolving a key, to verify the JWS with.
* @param options JWS Verify options.
*
* @example
* ```
* // ESM import
* import generalVerify from 'jose/jws/general/verify'
* ```
*
* @example
* ```
* // CJS import
* const { default: generalVerify } = require('jose/jws/general/verify')
* ```
*
* @example
* ```
* // usage
* import parseJwk from 'jose/jwk/parse'
*
* const decoder = new TextDecoder()
* const jws = {
* payload: 'SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4',
* signatures: [
* {
* signature: 'FVVOXwj6kD3DqdfD9yYqfT2W9jv-Nop4kOehp_DeDGNB5dQNSPRvntBY6xH3uxlCxE8na9d_kyhYOcanpDJ0EA',
* protected: 'eyJhbGciOiJFUzI1NiJ9'
* }
* ]
* }
* const publicKey = await parseJwk({
* alg: 'ES256',
* crv: 'P-256',
* kty: 'EC',
* x: 'ySK38C1jBdLwDsNWKzzBHqKYEE5Cgv-qjWvorUXk9fw',
* y: '_LeQBw07cf5t57Iavn4j-BqJsAD1dpoz8gokd3sBsOo'
* })
*
* const { payload, protectedHeader } = await generalVerify(jws, publicKey)
*
* console.log(protectedHeader)
* console.log(decoder.decode(payload))
* ```
*/
export default async function generalVerify(
jws: GeneralJWSInput,
key: KeyLike | GeneralVerifyGetKey,
options?: VerifyOptions,
): Promise<GeneralVerifyResult> {
if (!isObject(jws)) {
throw new JWSInvalid('General JWS must be an object')
}

if (!Array.isArray(jws.signatures) || !jws.signatures.every(isObject)) {
throw new JWSInvalid('JWS Signatures missing or incorrect type')
}

// eslint-disable-next-line no-restricted-syntax
for (const signature of jws.signatures) {
const flattened = { payload: jws.payload, ...signature }
try {
// eslint-disable-next-line no-await-in-loop
return await verify(flattened, <Parameters<typeof verify>[1]>key, options)
} catch {
//
}
}
throw new JWSSignatureVerificationFailed()
}

export type { KeyLike, GeneralJWSInput, VerifyOptions }
22 changes: 22 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,26 @@ export interface FlattenedJWSInput {
signature: string
}

/**
* General JWS definition for verify function inputs, allows payload as
* Uint8Array for detached signature validation.
*/
export interface GeneralJWSInput {
/**
* The "payload" member MUST be present and contain the value
* BASE64URL(JWS Payload). When RFC7797 "b64": false is used
* the value passed may also be a Uint8Array.
*/
payload: string | Uint8Array

/**
* The "signatures" member value MUST be an array of JSON objects.
* Each object represents a signature or MAC over the JWS Payload and
* the JWS Protected Header.
*/
signatures: Omit<FlattenedJWSInput, 'payload'>[]
}

/**
* Flattened JWS definition. Payload is an optional return property, it
* is not returned when JWS Unencoded Payload Option
Expand Down Expand Up @@ -549,6 +569,8 @@ export interface FlattenedVerifyResult {
unprotectedHeader?: JWSHeaderParameters
}

export interface GeneralVerifyResult extends FlattenedVerifyResult {}

export interface CompactVerifyResult {
/**
* JWS Payload.
Expand Down
81 changes: 81 additions & 0 deletions test/jws/general.verify.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable no-param-reassign */
import test from 'ava';

const root = !('WEBCRYPTO' in process.env) ? '#dist' : '#dist/webcrypto';
Promise.all([
import(`${root}/jws/flattened/sign`),
import(`${root}/jws/general/verify`),
import(`${root}/util/random`),
]).then(
([{ default: FlattenedSign }, { 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(32));
});

test('JWS format validation', async (t) => {
const flattenedJws = await new FlattenedSign(t.context.plaintext)
.setProtectedHeader({ bar: 'baz' })
.setUnprotectedHeader({ alg: 'HS256' })
.sign(t.context.secret);

{
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: flattenedJws.payload, signatures: [] };

await t.throwsAsync(generalVerify(jws, t.context.secret), {
message: 'signature verification failed',
code: 'ERR_JWS_SIGNATURE_VERIFICATION_FAILED',
});
}

{
const { payload, ...signature } = flattenedJws;
const jws = { payload, signatures: [signature] };

await t.notThrowsAsync(generalVerify(jws, t.context.secret));
}

{
const { payload, ...signature } = flattenedJws;
const jws = { payload, signatures: [signature, {}] };

await t.notThrowsAsync(generalVerify(jws, t.context.secret));
}

{
const { payload, ...signature } = flattenedJws;
const jws = { payload, signatures: [{}, signature] };

await t.notThrowsAsync(generalVerify(jws, t.context.secret));
}
});
},
(err) => {
test('failed to import', (t) => {
console.error(err);
t.fail();
});
},
);
1 change: 1 addition & 0 deletions tsconfig/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"../src/jws/compact/verify.ts",
"../src/jws/flattened/sign.ts",
"../src/jws/flattened/verify.ts",
"../src/jws/general/verify.ts",

"../src/jwk/parse.ts",
"../src/jwk/thumbprint.ts",
Expand Down

0 comments on commit 55b7781

Please sign in to comment.