From 8fd01d6cbeb0a55198c97161130069776fb9f836 Mon Sep 17 00:00:00 2001 From: Joel Thorstensson Date: Fri, 24 Nov 2023 15:45:25 +0300 Subject: [PATCH] JWS canon --- packages/varsig/src/canons/eip712.ts | 2 +- packages/varsig/src/canons/jws.ts | 138 ++++++++++++++++++--------- pnpm-lock.yaml | 22 +++-- 3 files changed, 111 insertions(+), 51 deletions(-) diff --git a/packages/varsig/src/canons/eip712.ts b/packages/varsig/src/canons/eip712.ts index 4455896e..0c661c8b 100644 --- a/packages/varsig/src/canons/eip712.ts +++ b/packages/varsig/src/canons/eip712.ts @@ -252,7 +252,7 @@ export function fromOriginal({ varintes.encode(0xe7)[0], // key type recoveryBit, varintes.encode(0x1b)[0], // hash type - varintes.encode(0xe712)[0], // canonicalizer codec + varintes.encode(SIGIL)[0], // canonicalizer codec metadataLength, metadataBytes, signatureBytes, diff --git a/packages/varsig/src/canons/jws.ts b/packages/varsig/src/canons/jws.ts index 184f1733..0bcaaa67 100644 --- a/packages/varsig/src/canons/jws.ts +++ b/packages/varsig/src/canons/jws.ts @@ -1,43 +1,95 @@ -// import type { GeneralJWS } from 'dids' -// import * as uint8arrays from 'uint8arrays' -// import { toBytes } from '../bytes.js' -// import { ENCODING, HASHING, SIGNING } from '../varsig.js' -// -// export function encode(jws: GeneralJWS) { -// const signature0 = jws.signatures[0] -// // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -// const protectedHeader = JSON.parse( -// uint8arrays.toString(uint8arrays.fromString(signature0.protected, 'base64url')) -// ) -// const payload = JSON.parse(uint8arrays.toString(uint8arrays.fromString(jws.payload, 'base64url'))) -// const signature = uint8arrays.fromString(signature0.signature, 'base64url') -// // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -// switch (protectedHeader.alg) { -// case 'EdDSA': { -// const sig = toBytes({ -// encoding: ENCODING.JWT, -// hashing: HASHING.SHA2_256, -// signing: SIGNING.ED25519, -// signature: signature, -// }) -// delete protectedHeader.typ -// delete protectedHeader.alg -// return { -// _header: protectedHeader, -// ...payload, -// _signature: sig, -// } -// } -// case 'ES256': { -// const sig = toBytes({ -// encoding: ENCODING.JWT, -// hashing: HASHING.SHA2_256, -// signing: SIGNING.RSA -// }) -// } -// } -// return { -// _header: {}, -// _sig: {}, -// } -// } +import { CanonicalizationAlgo } from '../canonicalization.js' +import { BytesTape } from '../bytes-tape.js' +import { HashingAlgo } from '../hashing' +import { SigningKind } from '../signing' +import * as varintes from 'varintes' +import * as uint8arrays from 'uint8arrays' +import { encode, decode } from '@ipld/dag-json' + +type IpldNode = Record +type IpldNodeSigned = IpldNode & { _sig: Uint8Array } + +const KEY_TYPE_BY_ALG_CRV: Record> = { + ES256: { default: 0x1200 }, + EdDSA: { ed448: 0x1203, ed25519: 0xec, default: 0xec }, + ES256K: { default: 0xe7 }, +} +const HASH_BY_KEY_TYPE: Record = { + 0x1200: 0x12, + 0xe7: 0x12, + 0xec: 0x12, + 0x1203: 0x19, +} + +const toB64u = (bytes: Uint8Array) => uint8arrays.toString(bytes, 'base64url') +const fromB64u = (b64u: string) => uint8arrays.fromString(b64u, 'base64url') + + +const SIGIL = 0x7053 // jose + +export const JWS = { SIGIL, prepareCanonicalization, fromOriginal } + +export function prepareCanonicalization( + tape: BytesTape, + hashType: HashingAlgo, + keyType: SigningKind +): CanonicalizationAlgo { + const protectedLength = tape.readVarint() + const protectedBytes = tape.read(protectedLength) + const protected1 = JSON.parse(uint8arrays.toString(protectedBytes)) + + const keyTypeFromProtected = findKeyType(protected1) + if (keyType !== keyTypeFromProtected) throw new Error(`Key type missmatch: ${keyType}, ${keyTypeFromProtected}`) + if (hashType !== HASH_BY_KEY_TYPE[keyType]) throw new Error(`Hash type missmatch: ${hashType}, ${HASH_BY_KEY_TYPE[keyType]}`) + + const can = (node: IpldNode) => { + // encode node using dag-json from multiformats + const payloadB64u = toB64u(uint8arrays.fromString(JSON.stringify(encode(node)))) + const protectedB64u = toB64u(protectedBytes) + return uint8arrays.fromString(`${protectedB64u}.${payloadB64u}`) + } + can.kind = SIGIL + can.original = (node: IpldNode, signature: Uint8Array) => { + const payloadB64u = toB64u(encode(node)) + const protectedB64u = toB64u(protectedBytes) + const signatureB64u = toB64u(signature) + return `${protectedB64u}.${payloadB64u}.${signatureB64u}` + } + return can +} + +export function fromOriginal(jws: string): IpldNodeSigned { + const [protectedB64u, payloadB64u, signatureB64u] = jws.split('.') + const node = decode(fromB64u(payloadB64u)) as IpldNode + const protectedBytes = fromB64u(protectedB64u) + const protected1 = JSON.parse(uint8arrays.toString(protectedBytes)) + const protectedLength = varintes.encode(protectedBytes.length)[0] + const signature = fromB64u(signatureB64u) + + const keyType = findKeyType(protected1) + const hashType = HASH_BY_KEY_TYPE[keyType] + + // TODO - this doesn't currently support RSA signatures + const varsig = uint8arrays.concat([ + new Uint8Array([0x34]), // varsig sigil + varintes.encode(keyType)[0], // key type + varintes.encode(hashType)[0], // hash type + varintes.encode(SIGIL)[0], // canonicalizer codec + protectedLength, + protectedBytes, + signature, + ]) + return { ...node, _sig: varsig } +} + +interface ProtectedHeader { + alg: string + crv?: string +} + +function findKeyType({ alg, crv }: ProtectedHeader): number { + if (!alg) throw new Error(`Missing alg in protected header`) + const keyType = KEY_TYPE_BY_ALG_CRV[alg][crv || 'default'] + if (!keyType) throw new Error(`Unsupported alg: ${alg}, or crv: ${crv}`) + return keyType +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87357e56..c483ddd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -573,6 +573,9 @@ importers: packages/varsig: dependencies: + '@ipld/dag-json': + specifier: ^10.1.5 + version: 10.1.5 '@noble/curves': specifier: ^1.2.0 version: 1.2.0 @@ -4471,12 +4474,12 @@ packages: cborg: 1.10.2 multiformats: 11.0.2 - /@ipld/dag-json@10.1.0: - resolution: {integrity: sha512-2rSvzDyGxx1NC24IsqKFTSXzAfUBlniZQRT15PEN+i177KEBsCXPfxuN/DweGIfmj3YceNyR8XOJT47pRZu7Cg==} + /@ipld/dag-json@10.1.5: + resolution: {integrity: sha512-AIIDRGPgIqVG2K1O42dPDzNOfP0YWV/suGApzpF+YWZLwkwdGVsxjmXcJ/+rwOhRGdjpuq/xQBKPCu1Ao6rdOQ==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} dependencies: - cborg: 1.10.2 - multiformats: 11.0.2 + cborg: 4.0.3 + multiformats: 12.1.3 dev: false /@ipld/dag-pb@4.0.3: @@ -12385,7 +12388,7 @@ packages: '@chainsafe/libp2p-noise': 11.0.4 '@ipld/car': 5.1.1 '@ipld/dag-cbor': 9.0.1 - '@ipld/dag-json': 10.1.0 + '@ipld/dag-json': 10.1.5 '@ipld/dag-pb': 4.0.3 '@libp2p/bootstrap': 6.0.3 '@libp2p/crypto': 1.0.17 @@ -12467,7 +12470,7 @@ packages: deprecated: js-IPFS has been deprecated in favour of Helia - please see https://github.com/ipfs/js-ipfs/issues/4336 for details dependencies: '@ipld/dag-cbor': 9.0.1 - '@ipld/dag-json': 10.1.0 + '@ipld/dag-json': 10.1.5 '@ipld/dag-pb': 4.0.3 '@libp2p/logger': 2.1.1 '@libp2p/peer-id': 2.0.3 @@ -14860,6 +14863,11 @@ packages: resolution: {integrity: sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg==} engines: {node: '>=16.0.0', npm: '>=7.0.0'} + /multiformats@12.1.3: + resolution: {integrity: sha512-eajQ/ZH7qXZQR2AgtfpmSMizQzmyYVmCql7pdhldPuYQi4atACekbJaQplk6dWyIi10jCaFnd6pqvcEFXjbaJw==} + engines: {node: '>=16.0.0', npm: '>=7.0.0'} + dev: false + /multiformats@9.9.0: resolution: {integrity: sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==}