diff --git a/package-lock.json b/package-lock.json index c397812..cee6740 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@transmute/cose", - "version": "0.2.11", + "version": "0.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@transmute/cose", - "version": "0.2.11", + "version": "0.2.12", "license": "Apache-2.0", "dependencies": { "@peculiar/x509": "^1.9.7", diff --git a/package.json b/package.json index b805f51..8971de2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@transmute/cose", - "version": "0.2.11", + "version": "0.2.12", "description": "COSE and related work.", "main": "./dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/cose/Params.ts b/src/cose/Params.ts index 01612bf..637134a 100644 --- a/src/cose/Params.ts +++ b/src/cose/Params.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + // This module is just just a limited set of the IANA registries, // exposed to make Map initialization more readable @@ -96,7 +98,8 @@ export const Hash = { } export const Signature = { - 'ES256': -7 + 'ES256': -7, + 'ES384': -35 } @@ -115,26 +118,32 @@ export const Direct = { 'HPKE-Base-P256-SHA256-AES128GCM': 35 } -export const EC2 = 2 -export const KeyTypes = { - EC2 -} export const KeyType = 1 export const KeyAlg = 3 -export const KeyCurve = -1 +export const KeyId = 2 -export const Epk = { +export const Key = { Kty: KeyType, - Crv: KeyCurve, - Alg: KeyAlg + Alg: KeyAlg, + Kid: KeyId } -export const Key = { - Kty: KeyType, - Crv: KeyCurve, - Alg: KeyAlg +export const Epk = { + ...Key +} + +export const KeyTypes = { + EC2: 2 +} + +export const EC2 = { + ...Key, + Crv: -1, + X: -2, + Y: -3, + D: -4 } export const Curves = { diff --git a/src/cose/key/convertCoseKeyToJsonWebKey.ts b/src/cose/key/convertCoseKeyToJsonWebKey.ts index f7fd85a..e251d99 100644 --- a/src/cose/key/convertCoseKeyToJsonWebKey.ts +++ b/src/cose/key/convertCoseKeyToJsonWebKey.ts @@ -1,27 +1,24 @@ import { base64url, calculateJwkThumbprint } from "jose"; import { CoseKey } from "."; - -import { IANACOSEAlgorithms } from '../algorithms'; import { IANACOSEEllipticCurves } from '../elliptic-curves'; -const algorithms = Object.values(IANACOSEAlgorithms) const curves = Object.values(IANACOSEEllipticCurves) import { formatJwk } from "./formatJwk"; +import { iana } from "../../iana"; +import { EC2, Key, KeyTypes } from "../Params"; export const convertCoseKeyToJsonWebKey = async (coseKey: CoseKey): Promise => { - const kty = coseKey.get(1) as number - const kid = coseKey.get(2) - const alg = coseKey.get(3) - const crv = coseKey.get(-1) - // kty EC, kty: EK - if (![2, 5].includes(kty)) { + const kty = coseKey.get(Key.Kty) as number + // kty EC2 + if (![KeyTypes.EC2].includes(kty)) { throw new Error('This library requires does not support the given key type') } - const foundAlgorithm = algorithms.find((param) => { - return param.Value === `${alg}` - }) + const kid = coseKey.get(Key.Kid) + const alg = coseKey.get(Key.Alg) + const crv = coseKey.get(EC2.Crv) + const foundAlgorithm = iana["COSE Algorithms"].getByValue(alg as number) if (!foundAlgorithm) { throw new Error('This library requires keys to use fully specified algorithms') } @@ -36,9 +33,9 @@ export const convertCoseKeyToJsonWebKey = async (coseKey: CoseKey): Promise(jwk: PublicKeyJwk | SecretKeyJwk): Promise => { +export const convertJsonWebKeyToCoseKey = async (jwk: PublicKeyJwk | PrivateKeyJwk): Promise => { const { kty } = jwk let coseKty = `${kty}` as 'OKP' | 'EC' | 'EC2'; // evidence of terrible design. @@ -82,9 +80,7 @@ export const convertJsonWebKeyToCoseKey = async (jwk: PublicKeyJwk | SecretKe } case 'alg': { if (foundCommonParam) { - const foundAlgorithm = algorithms.find((param) => { - return param.Name === value - }) + const foundAlgorithm = iana['COSE Algorithms'].getByName(value) if (foundAlgorithm) { coseKey.set(label, parseInt(foundAlgorithm.Value, 10)) } else { diff --git a/src/cose/key/generate.ts b/src/cose/key/generate.ts index 007a781..69f155b 100644 --- a/src/cose/key/generate.ts +++ b/src/cose/key/generate.ts @@ -6,8 +6,9 @@ import { IANACOSEAlgorithms } from "../algorithms" import { CoseKey } from '.' + export type CoseKeyAgreementAlgorithms = 'ECDH-ES+A128KW' -export type CoseSignatureAlgorithms = 'ES256' | 'ES384' | 'ES512' +export type CoseSignatureAlgorithms = 'ES256' | 'ES384' | 'ES512' | 'ESP256' | 'ESP384' export type ContentTypeOfJsonWebKey = 'application/jwk+json' export type ContentTypeOfCoseKey = 'application/cose-key' export type PrivateKeyContentType = ContentTypeOfCoseKey | ContentTypeOfJsonWebKey @@ -18,17 +19,25 @@ import { thumbprint } from "./thumbprint" import { formatJwk } from './formatJwk' +import { iana } from '../../iana' +import { Key } from "../Params" export const generate = async (alg: CoseSignatureAlgorithms, contentType: PrivateKeyContentType = 'application/jwk+json'): Promise => { - const knownAlgorithm = Object.values(IANACOSEAlgorithms).find(( + let knownAlgorithm = Object.values(IANACOSEAlgorithms).find(( entry ) => { return entry.Name === alg }) + if (!knownAlgorithm) { + knownAlgorithm = iana["COSE Algorithms"].getByName(alg) + } if (!knownAlgorithm) { throw new Error('Algorithm is not supported.') } - const cryptoKeyPair = await generateKeyPair(knownAlgorithm.Name, { extractable: true }); + const cryptoKeyPair = await generateKeyPair( + iana["COSE Algorithms"]["less-specified"](knownAlgorithm.Name), + { extractable: true } + ); const privateKeyJwk = await exportJWK(cryptoKeyPair.privateKey) const jwkThumbprint = await calculateJwkThumbprint(privateKeyJwk) privateKeyJwk.kid = jwkThumbprint @@ -40,7 +49,7 @@ export const generate = async (alg: CoseSignatureAlgorithms, contentType: Pri delete privateKeyJwk.kid; const secretKeyCoseKey = await convertJsonWebKeyToCoseKey(privateKeyJwk) const coseKeyThumbprint = await thumbprint.calculateCoseKeyThumbprint(secretKeyCoseKey) - secretKeyCoseKey.set(2, coseKeyThumbprint) + secretKeyCoseKey.set(Key.Kid, coseKeyThumbprint) return secretKeyCoseKey as T } throw new Error('Unsupported content type.') diff --git a/src/cose/key/index.ts b/src/cose/key/index.ts index df2b9af..800acc7 100644 --- a/src/cose/key/index.ts +++ b/src/cose/key/index.ts @@ -1,9 +1,9 @@ -import { PublicKeyJwk, SecretKeyJwk } from '../sign1' +import { PublicKeyJwk, PrivateKeyJwk } from '../sign1' -export type JsonWebKey = SecretKeyJwk | PublicKeyJwk +export type JsonWebKey = PrivateKeyJwk | PublicKeyJwk export type CoseMapKey = string | number export type CoseMapValue = Uint8Array | ArrayBuffer | string | number | Map diff --git a/src/cose/key/publicFromPrivate.ts b/src/cose/key/publicFromPrivate.ts index bfcdf23..a3eae73 100644 --- a/src/cose/key/publicFromPrivate.ts +++ b/src/cose/key/publicFromPrivate.ts @@ -1,8 +1,9 @@ import { CoseKey } from "."; -import { SecretKeyJwk } from "../sign1"; +import { EC2, Key, KeyTypes } from "../Params"; +import { PrivateKeyJwk } from "../sign1"; -export const extracePublicKeyJwk = (privateKeyJwk: SecretKeyJwk) => { +export const extractPublicKeyJwk = (privateKeyJwk: PrivateKeyJwk) => { if (privateKeyJwk.kty !== 'EC') { throw new Error('Only EC keys are supported') } @@ -13,19 +14,19 @@ export const extracePublicKeyJwk = (privateKeyJwk: SecretKeyJwk) => { export const extractPublicCoseKey = (secretKey: CoseKey) => { const publicCoseKeyMap = new Map(secretKey) - if (publicCoseKeyMap.get(1) !== 2) { + if (publicCoseKeyMap.get(Key.Kty) !== KeyTypes.EC2) { throw new Error('Only EC2 keys are supported') } - if (!publicCoseKeyMap.get(-4)) { + if (!publicCoseKeyMap.get(EC2.D)) { throw new Error('privateKey is not a secret / private key (has no d / -4)') } - publicCoseKeyMap.delete(-4); + publicCoseKeyMap.delete(EC2.D); return publicCoseKeyMap } -export const publicFromPrivate = (secretKey: SecretKeyJwk | CoseKey) => { - if ((secretKey as any).kty) { - return extracePublicKeyJwk(secretKey as SecretKeyJwk) as T +export const publicFromPrivate = (secretKey: PrivateKeyJwk | CoseKey) => { + if ((secretKey as PrivateKeyJwk).kty) { + return extractPublicKeyJwk(secretKey as PrivateKeyJwk) as T } return extractPublicCoseKey(secretKey as CoseKey) as T } \ No newline at end of file diff --git a/src/cose/key/serialize.ts b/src/cose/key/serialize.ts index aa509ee..bc5e4e3 100644 --- a/src/cose/key/serialize.ts +++ b/src/cose/key/serialize.ts @@ -5,7 +5,7 @@ import { CoseKey } from '.' export const serialize = (key: JWK | CoseKey) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((key as any).kty) { + if ((key as JWK).kty) { return JSON.stringify(key, null, 2) } return encode(key) diff --git a/src/cose/key/thumbprint.ts b/src/cose/key/thumbprint.ts index 9a01c14..c68b6b0 100644 --- a/src/cose/key/thumbprint.ts +++ b/src/cose/key/thumbprint.ts @@ -3,13 +3,18 @@ import { calculateJwkThumbprint, calculateJwkThumbprintUri, base64url } from "jo import { encodeCanonical } from "../../cbor"; import subtleCryptoProvider from "../../crypto/subtleCryptoProvider"; +import { EC2, Key, KeyTypes } from "../Params"; +import { CoseKey } from "."; // https://www.ietf.org/archive/id/draft-ietf-cose-key-thumbprint-01.html#section-6 -const calculateCoseKeyThumbprint = async (coseKey: Map): Promise => { +const calculateCoseKeyThumbprint = async (coseKey: CoseKey): Promise => { + if (coseKey.get(Key.Kty) !== KeyTypes.EC2) { + throw new Error('Unsupported key type (Only EC2 are supported') + } const onlyRequiredMap = new Map() - const requriedKeys = [1, -1, -2, -3] + const requiredKeys = [EC2.Kty, EC2.Crv, EC2.X, EC2.Y] for (const [key, value] of coseKey.entries()) { - if (requriedKeys.includes(key as number)) { + if (requiredKeys.includes(key as number)) { onlyRequiredMap.set(key, value) } } @@ -19,7 +24,7 @@ const calculateCoseKeyThumbprint = async (coseKey: Map): Promise): Promise => { +const calculateCoseKeyThumbprintUri = async (coseKey: CoseKey): Promise => { const prefix = `urn:ietf:params:oauth:ckt:sha-256` const digest = await calculateCoseKeyThumbprint(coseKey) return `${prefix}:${base64url.encode(new Uint8Array(digest))}` diff --git a/src/cose/receipt/add.ts b/src/cose/receipt/add.ts index c7bd351..07468c4 100644 --- a/src/cose/receipt/add.ts +++ b/src/cose/receipt/add.ts @@ -1,4 +1,5 @@ import { decodeFirstSync, toArrayBuffer, encodeAsync, Tagged, Sign1Tag } from '../../cbor' +import { Receipts } from '../Params'; import { CoseSign1Bytes } from "../sign1"; export const add = async (signature: CoseSign1Bytes, receipt: CoseSign1Bytes): Promise => { @@ -10,8 +11,8 @@ export const add = async (signature: CoseSign1Bytes, receipt: CoseSign1Bytes): P value[1] = new Map(); } // unprotected header - const receipts = value[1].get(394) || []; // see https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/ + const receipts = value[1].get(Receipts) || []; // see https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/ receipts.push(receipt) - value[1].set(394, receipts) + value[1].set(Receipts, receipts) return toArrayBuffer(await encodeAsync(new Tagged(Sign1Tag, value), { canonical: true })); } \ No newline at end of file diff --git a/src/cose/receipt/consistency/issue.ts b/src/cose/receipt/consistency/issue.ts index dd60a62..2142217 100644 --- a/src/cose/receipt/consistency/issue.ts +++ b/src/cose/receipt/consistency/issue.ts @@ -1,7 +1,7 @@ import { CoMETRE } from '@transmute/rfc9162' -import { cbor, Protected, Unprotected, VerifiableDataProofTypes } from '../../..' +import { cbor, Protected, Unprotected, VerifiableDataProofTypes, VerifiableDataStructures } from '../../..' import { CoseSign1Bytes, CoseSign1Signer, ProtectedHeaderMap } from "../../sign1" import { toArrayBuffer } from '../../../cbor' @@ -16,7 +16,7 @@ export type RequestIssueConsistencyReceipt = { export const issue = async (req: RequestIssueConsistencyReceipt) => { const { protectedHeader, receipt, entries, signer } = req; const consistencyVds = protectedHeader.get(Protected.VerifiableDataStructure) - if (consistencyVds !== 1) { + if (consistencyVds !== VerifiableDataStructures['RFC9162-Binary-Merkle-Tree']) { throw new Error('Unsupported verifiable data structure. See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs') } @@ -28,7 +28,7 @@ export const issue = async (req: RequestIssueConsistencyReceipt) => { const [protectedHeaderBytes, unprotectedHeaderMap, payload] = value const receiptProtectedHeader = cbor.decode(protectedHeaderBytes) const inclusionVds = receiptProtectedHeader.get(Protected.VerifiableDataStructure); - if (inclusionVds !== 1) { + if (inclusionVds !== VerifiableDataStructures['RFC9162-Binary-Merkle-Tree']) { throw new Error('Unsupported verifiable data structure. See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs') } diff --git a/src/cose/receipt/consistency/verify.ts b/src/cose/receipt/consistency/verify.ts index 2aa0081..29f57c7 100644 --- a/src/cose/receipt/consistency/verify.ts +++ b/src/cose/receipt/consistency/verify.ts @@ -1,7 +1,7 @@ import { CoMETRE } from '@transmute/rfc9162' -import { cbor, Protected, Unprotected, VerifiableDataProofTypes } from '../../..' +import { cbor, Protected, Unprotected, VerifiableDataProofTypes, VerifiableDataStructures } from '../../..' import { CoseSign1Bytes, CoseSign1DetachedVerifier } from "../../sign1" @@ -21,7 +21,7 @@ export const verify = async (req: RequestVerifyConsistencyReceipt) => { const [protectedHeaderBytes, unprotectedHeaderMap, payload] = value const protectedHeader = cbor.decode(protectedHeaderBytes) const vds = protectedHeader.get(Protected.VerifiableDataStructure); - if (vds !== 1) { + if (vds !== VerifiableDataStructures['RFC9162-Binary-Merkle-Tree']) { throw new Error('Unsupported verifiable data structure. See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs') } const proofs = unprotectedHeaderMap.get(Unprotected.VerifiableDataProofs) diff --git a/src/cose/receipt/get.ts b/src/cose/receipt/get.ts index fd2f91f..7ae264b 100644 --- a/src/cose/receipt/get.ts +++ b/src/cose/receipt/get.ts @@ -1,4 +1,5 @@ import { decodeFirstSync, Sign1Tag } from '../../cbor' +import { Receipts } from '../Params'; import { CoseSign1Bytes } from "../sign1"; export const get = async (signature: CoseSign1Bytes): Promise => { @@ -10,6 +11,6 @@ export const get = async (signature: CoseSign1Bytes): Promise return [] } // unprotected header - const receipts = value[1].get(394) || []; // see https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/ + const receipts = value[1].get(Receipts) || []; // see https://datatracker.ietf.org/doc/draft-ietf-scitt-architecture/ return receipts } \ No newline at end of file diff --git a/src/cose/receipt/inclusion/issue.ts b/src/cose/receipt/inclusion/issue.ts index 5bb0b8d..56fdda5 100644 --- a/src/cose/receipt/inclusion/issue.ts +++ b/src/cose/receipt/inclusion/issue.ts @@ -1,7 +1,7 @@ import { CoMETRE } from '@transmute/rfc9162' -import { cbor, Protected, Unprotected, VerifiableDataProofTypes } from '../../..' +import { cbor, Protected, Unprotected, VerifiableDataProofTypes, VerifiableDataStructures } from '../../..' import { CoseSign1Signer, ProtectedHeaderMap } from "../../sign1" @@ -15,7 +15,7 @@ export type RequestIssueInclusionReceipt = { export const issue = async (req: RequestIssueInclusionReceipt) => { const { protectedHeader, entry, entries, signer } = req; const vds = protectedHeader.get(Protected.VerifiableDataStructure) - if (vds !== 1) { + if (vds !== VerifiableDataStructures['RFC9162-Binary-Merkle-Tree']) { throw new Error('Unsupported verifiable data structure. See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs') } const root = await CoMETRE.RFC9162_SHA256.root(entries) diff --git a/src/cose/receipt/inclusion/verify.ts b/src/cose/receipt/inclusion/verify.ts index d49c4f6..4fd14d2 100644 --- a/src/cose/receipt/inclusion/verify.ts +++ b/src/cose/receipt/inclusion/verify.ts @@ -1,7 +1,7 @@ import { CoMETRE } from '@transmute/rfc9162' -import { cbor, Protected, Unprotected, VerifiableDataProofTypes } from '../../..' +import { cbor, Protected, Unprotected, VerifiableDataProofTypes, VerifiableDataStructures } from '../../..' import { CoseSign1Bytes, CoseSign1DetachedVerifier } from "../../sign1" @@ -20,7 +20,7 @@ export const verify = async (req: RequestVerifyInclusionReceipt) => { const [protectedHeaderBytes, unprotectedHeaderMap, payload] = value const protectedHeader = cbor.decode(protectedHeaderBytes) const vds = protectedHeader.get(Protected.VerifiableDataStructure); - if (vds !== 1) { + if (vds !== VerifiableDataStructures['RFC9162-Binary-Merkle-Tree']) { throw new Error('Unsupported verifiable data structure. See https://datatracker.ietf.org/doc/draft-ietf-cose-merkle-tree-proofs') } const proofs = unprotectedHeaderMap.get(Unprotected.VerifiableDataProofs) diff --git a/src/cose/sign1/getAlgFromVerificationKey.ts b/src/cose/sign1/getAlgFromVerificationKey.ts deleted file mode 100644 index 552ce1e..0000000 --- a/src/cose/sign1/getAlgFromVerificationKey.ts +++ /dev/null @@ -1,16 +0,0 @@ - -import { IANACOSEAlgorithms } from '../algorithms'; - -const algorithms = Object.values(IANACOSEAlgorithms) - -const getAlgFromVerificationKey = (alg: string): number => { - const foundAlg = algorithms.find((entry) => { - return entry.Name === alg - }) - if (!foundAlg) { - throw new Error('This library requires keys to contain fully specified algorithms') - } - return parseInt(foundAlg.Value, 10) -} - -export default getAlgFromVerificationKey \ No newline at end of file diff --git a/src/cose/sign1/getDigestFromVerificationKey.ts b/src/cose/sign1/getDigestFromVerificationKey.ts index 95f1469..ab2be54 100644 --- a/src/cose/sign1/getDigestFromVerificationKey.ts +++ b/src/cose/sign1/getDigestFromVerificationKey.ts @@ -1,10 +1,15 @@ const joseToCose = new Map() + joseToCose.set('ES256', `SHA-256`) joseToCose.set('ES384', `SHA-384`) joseToCose.set('ES512', `SHA-512`) +// fully specified +joseToCose.set('ESP256', `SHA-256`) +joseToCose.set('ESP384', `SHA-384`) + const getDigestFromVerificationKey = (alg: string): string => { const digestAlg = joseToCose.get(alg) if (!digestAlg) { diff --git a/src/cose/sign1/hashEnvelopeSigner.ts b/src/cose/sign1/hashEnvelopeSigner.ts index 6fbe693..975d877 100644 --- a/src/cose/sign1/hashEnvelopeSigner.ts +++ b/src/cose/sign1/hashEnvelopeSigner.ts @@ -6,9 +6,7 @@ import { RequestCoseSign1Signer, RequestCoseSign1 } from "./types" // https://datatracker.ietf.org/doc/draft-steele-cose-hash-envelope/ - -import { Protected } from "../Params"; - +import { Hash, Protected } from "../Params"; export const hash = { signer: ({ remote }: RequestCoseSign1Signer) => { @@ -16,7 +14,7 @@ export const hash = { sign: async ({ protectedHeader, unprotectedHeader, payload }: RequestCoseSign1): Promise => { const subtle = await subtleCryptoProvider(); const hashEnvelopeAlgorithm = protectedHeader.get(Protected.PayloadHashAlgorithm) - if (hashEnvelopeAlgorithm !== -16) { + if (hashEnvelopeAlgorithm !== Hash.SHA256) { throw new Error('Unsupported hash envelope algorithm (-16 is only one supported)') } const payloadHash = await subtle.digest("SHA-256", payload) diff --git a/src/cose/sign1/payload.ts b/src/cose/sign1/payload.ts index cbc7d4d..ce79455 100644 --- a/src/cose/sign1/payload.ts +++ b/src/cose/sign1/payload.ts @@ -1,6 +1,6 @@ -import { decodeFirst, decodeFirstSync, encode, EMPTY_BUFFER } from '../../cbor' -import { DecodedToBeSigned, ProtectedHeaderMap } from './types' +import { decodeFirst, encode } from '../../cbor' + export const attach = async (coseSign1Bytes: ArrayBuffer, payload: ArrayBuffer) => { diff --git a/src/cose/sign1/types.ts b/src/cose/sign1/types.ts index 02add4c..fc4de08 100644 --- a/src/cose/sign1/types.ts +++ b/src/cose/sign1/types.ts @@ -8,8 +8,8 @@ export type DecodedCoseSign1 = { value: CoseSign1Structure } -export type SecretKeyJwk = JsonWebKey & { d: string, kid?: string } -export type PublicKeyJwk = Omit +export type PrivateKeyJwk = JsonWebKey & { d: string, kid?: string } +export type PublicKeyJwk = Omit export type RequestCoseSign1Signer = { remote: { diff --git a/src/cose/sign1/verifier.ts b/src/cose/sign1/verifier.ts index e330cba..7bfc309 100644 --- a/src/cose/sign1/verifier.ts +++ b/src/cose/sign1/verifier.ts @@ -1,15 +1,18 @@ import { decodeFirst, decodeFirstSync, encode, EMPTY_BUFFER } from '../../cbor' import { RequestCoseSign1Verifier, RequestCoseSign1Verify } from './types' -import getAlgFromVerificationKey from './getAlgFromVerificationKey' + import { DecodedToBeSigned, ProtectedHeaderMap } from './types' import rawVerifier from '../../crypto/verifier' +import { iana } from '../../iana' +import { Protected } from '../Params' + const verifier = ({ resolver }: RequestCoseSign1Verifier) => { return { verify: async ({ coseSign1, externalAAD }: RequestCoseSign1Verify): Promise => { const publicKeyJwk = await resolver.resolve(coseSign1) - const algInPublicKey = getAlgFromVerificationKey(`${publicKeyJwk.alg}`) + const algInPublicKey = parseInt(`${iana['COSE Algorithms'].getByName(`${publicKeyJwk.alg}`)?.Value}`, 10) const ecdsa = rawVerifier({ publicKeyJwk }) const obj = await decodeFirst(coseSign1); const signatureStructure = obj.value; @@ -19,9 +22,10 @@ const verifier = ({ resolver }: RequestCoseSign1Verifier) => { if (signatureStructure.length !== 4) { throw new Error('Expecting Array of length 4'); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars const [protectedHeaderBytes, _, payload, signature] = signatureStructure; const protectedHeaderMap: ProtectedHeaderMap = (!protectedHeaderBytes.length) ? new Map() : decodeFirstSync(protectedHeaderBytes); - const algInHeader = protectedHeaderMap.get(1) + const algInHeader = protectedHeaderMap.get(Protected.Alg) if (algInHeader !== algInPublicKey) { throw new Error('Verification key does not support algorithm: ' + algInHeader); } diff --git a/src/crypto/signer.ts b/src/crypto/signer.ts index 0c9c50e..c131cef 100644 --- a/src/crypto/signer.ts +++ b/src/crypto/signer.ts @@ -1,22 +1,24 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { toArrayBuffer } from '../cbor' -import { SecretKeyJwk } from '../cose/sign1' +import { PrivateKeyJwk } from '../cose/sign1' import subtleCryptoProvider from './subtleCryptoProvider' import getDigestFromVerificationKey from '../cose/sign1/getDigestFromVerificationKey' -const signer = ({ privateKeyJwk }: { privateKeyJwk: SecretKeyJwk }) => { +const signer = ({ privateKeyJwk }: { privateKeyJwk: PrivateKeyJwk }) => { const digest = getDigestFromVerificationKey(`${privateKeyJwk.alg}`) + const { alg, ...withoutAlg } = privateKeyJwk return { sign: async (toBeSigned: ArrayBuffer): Promise => { const subtle = await subtleCryptoProvider() const signingKey = await subtle.importKey( "jwk", - privateKeyJwk, + withoutAlg, { name: "ECDSA", - namedCurve: privateKeyJwk.crv, + namedCurve: withoutAlg.crv, }, true, ["sign"], diff --git a/src/crypto/verifier.ts b/src/crypto/verifier.ts index 67d8509..a8640b2 100644 --- a/src/crypto/verifier.ts +++ b/src/crypto/verifier.ts @@ -9,15 +9,17 @@ import { PublicKeyJwk } from '../cose/sign1' const verifier = ({ publicKeyJwk }: { publicKeyJwk: PublicKeyJwk }) => { const digest = getDigestFromVerificationKey(`${publicKeyJwk.alg}`) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { alg, ...withoutAlg } = publicKeyJwk return { verify: async (toBeSigned: ArrayBuffer, signature: ArrayBuffer): Promise => { const subtle = await subtleCryptoProvider() const verificationKey = await subtle.importKey( "jwk", - publicKeyJwk, + withoutAlg, { name: "ECDSA", - namedCurve: publicKeyJwk.crv, + namedCurve: withoutAlg.crv, }, true, ["verify"], diff --git a/src/iana/index.ts b/src/iana/index.ts new file mode 100644 index 0000000..75adc0b --- /dev/null +++ b/src/iana/index.ts @@ -0,0 +1,64 @@ + + +import { IANACOSEAlgorithms, IANACOSEAlgorithm } from '../cose/algorithms'; + +const algorithms = Object.values(IANACOSEAlgorithms) + +const ESP256 = { + Name: 'ESP256', + Value: '-9' +} as IANACOSEAlgorithm + +const ESP384 = { + Name: 'ESP384', + Value: '-48' +} as IANACOSEAlgorithm + +const fullySpecifiedByName = { + ESP256, + ESP384 +} as Record + +const fullySpecifiedByLabel = { + [ESP256.Value]: ESP256, + [ESP384.Value]: ESP384, +} as Record + +export const iana = { + 'COSE Algorithms': { + 'less-specified': (alg: string) => { + if (alg === 'ESP256') { + return 'ES256' + } + if (alg === 'ESP384') { + return 'ES384' + } + return alg + }, + getByName: (name: string) => { + const foundAlgorithm = algorithms.find((param) => { + return param.Name === name + }) + if (foundAlgorithm && foundAlgorithm.Name !== 'Unassigned') { + return foundAlgorithm + } + // extensions + if (fullySpecifiedByName[name]) { + return fullySpecifiedByName[name] + } + }, + getByValue: (value: number) => { + const foundAlgorithm = algorithms.find((param) => { + return param.Value === `${value}` + }) + if (foundAlgorithm && foundAlgorithm.Name !== 'Unassigned') { + return foundAlgorithm + } + // extensions + if (fullySpecifiedByLabel[`${value}`]) { + return fullySpecifiedByLabel[`${value}`] + } + } + } +} + diff --git a/src/x509/certificate.ts b/src/x509/certificate.ts index a4d6cfc..5be4798 100644 --- a/src/x509/certificate.ts +++ b/src/x509/certificate.ts @@ -1,15 +1,8 @@ import { exportJWK, exportPKCS8, importPKCS8 } from 'jose'; -import { ProtectedHeaderMap, PublicKeyJwk } from "../cose/sign1" - +import { PublicKeyJwk } from "../cose/sign1" import * as x509 from "@peculiar/x509"; - import { CoseSignatureAlgorithms } from '../cose/key'; - -import { IANACOSEAlgorithms, SecretKeyJwk, detached, RequestCoseSign1VerifyDetached } from '..'; - - -import { decodeFirstSync } from '../cbor' - +import { IANACOSEAlgorithms, PrivateKeyJwk, detached, RequestCoseSign1VerifyDetached, Hash } from '..'; import { crypto } from '..'; // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -29,6 +22,16 @@ const provide = async () => { const algTowebCryptoParams: Record = { + 'ESP256': { + name: "ECDSA", + hash: "SHA-256", + namedCurve: "P-256", + }, + 'ESP384': { + name: "ECDSA", + hash: "SHA-384", + namedCurve: "P-384", + }, 'ES256': { name: "ECDSA", hash: "SHA-256", @@ -52,12 +55,13 @@ export type RequestRootCertificate = { iss: string nbf: string exp: string + serial: string } // https://datatracker.ietf.org/doc/html/rfc9360#section-2-5.6.1 const thumbprint = async (cert: string): Promise<[number, ArrayBuffer]> => { const current = new x509.X509Certificate(cert) - return [-16, await current.getThumbprint('SHA-256')] + return [Hash.SHA256, await current.getThumbprint('SHA-256')] } export type RootCertificateResponse = { public: string, private: string } @@ -73,7 +77,7 @@ const root = async (req: RequestRootCertificate): Promise { + const publicKey = await cose.key.extractPublicCoseKey(privateKey) + expect(new TextDecoder().decode(await cose.attached + .verifier({ + resolver: { + resolve: async () => { + return cose.key.convertCoseKeyToJsonWebKey(publicKey) + } + } + }) + .verify({ + coseSign1: await cose.attached + .signer({ + remote: cose.crypto.signer({ + privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(privateKey) + }) + }) + .sign({ + protectedHeader: new Map([ + [cose.Protected.Alg, privateKey.get(cose.Key.Alg)] + ]), + payload: new TextEncoder().encode(message) + }) + }))).toBe(message) +} + +// https://datatracker.ietf.org/doc/draft-ietf-jose-fully-specified-algorithms/ + +const algorithms = ["ESP256", "ESP384"] as CoseSignatureAlgorithms[] + +algorithms.forEach((alg) => { + it(alg, async () => { + const privateKey = await cose.key.generate(alg, 'application/cose-key') + await helpTestSignAndVerify(privateKey) + }) +}) + + diff --git a/test/key.test.ts b/test/key.test.ts index 852720e..3fec1a9 100644 --- a/test/key.test.ts +++ b/test/key.test.ts @@ -3,12 +3,12 @@ import { base64url } from 'jose' import * as transmute from '../src' it('generate cose key', async () => { - const secretKeyJwk1 = await transmute.key.generate('ES256', 'application/jwk+json') + const secretKeyJwk1 = await transmute.key.generate('ES256', 'application/jwk+json') const secretKeyCose1 = await transmute.key.convertJsonWebKeyToCoseKey(secretKeyJwk1) - expect(secretKeyCose1.get(-1)).toBe(1) // crv : P-256 + expect(secretKeyCose1.get(transmute.EC2.Crv)).toBe(transmute.Curves.P256) // crv : P-256 const secretKeyCose2 = await transmute.key.generate('ES256', 'application/cose-key') - expect(secretKeyCose2.get(-1)).toBe(1) // crv : P-256 - const secretKeyJwk2 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose1) + expect(secretKeyCose2.get(transmute.EC2.Crv)).toBe(transmute.Curves.P256) // crv : P-256 + const secretKeyJwk2 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose1) expect(secretKeyJwk2.kid).toBe(secretKeyJwk1.kid) // text identifiers survive key conversion expect(secretKeyJwk2.alg).toBe(secretKeyJwk1.alg) expect(secretKeyJwk2.kty).toBe(secretKeyJwk1.kty) @@ -16,9 +16,9 @@ it('generate cose key', async () => { expect(secretKeyJwk2.x).toBe(secretKeyJwk1.x) expect(secretKeyJwk2.y).toBe(secretKeyJwk1.y) expect(secretKeyJwk2.d).toBe(secretKeyJwk1.d) - const secretKeyJwk3 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose1) + const secretKeyJwk3 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose1) const secretKeyCose3 = await transmute.key.convertJsonWebKeyToCoseKey(secretKeyJwk3) - const secretKeyJwk4 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose3) + const secretKeyJwk4 = await transmute.key.convertCoseKeyToJsonWebKey(secretKeyCose3) expect(secretKeyJwk4.kid).toBe(secretKeyJwk3.kid) // text identifiers survive key conversion }) @@ -45,14 +45,14 @@ it('generate thumbprints', async () => { }) it('public from private for JWK and cose key', async () => { - const privateKeyJwk = await transmute.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await transmute.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...expectedPublicKeyJwk } = privateKeyJwk const publicKeyJwk = transmute.key.publicFromPrivate(privateKeyJwk) expect(publicKeyJwk).toEqual(expectedPublicKeyJwk) const secretKeyCose = await transmute.key.generate('ES256', 'application/cose-key') const expectedPublicKeyCose = new Map(secretKeyCose.entries()) - expectedPublicKeyCose.delete(-4) + expectedPublicKeyCose.delete(transmute.EC2.D) const publicKeyCose = transmute.key.publicFromPrivate(secretKeyCose) expect(publicKeyCose).toEqual(expectedPublicKeyCose) }) \ No newline at end of file diff --git a/test/readme.test.ts b/test/readme.test.ts index 6c8517f..69e95c7 100644 --- a/test/readme.test.ts +++ b/test/readme.test.ts @@ -2,10 +2,10 @@ import fs from 'fs' import * as cose from '../src' it('readme', async () => { - const issuerSecretKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const issuerSecretKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') const issuerPublicKeyJwk = await cose.key.publicFromPrivate(issuerSecretKeyJwk) - const notarySecretKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const notarySecretKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') const notaryPublicKeyJwk = await cose.key.publicFromPrivate(notarySecretKeyJwk) const issuer = cose.detached.signer({ diff --git a/test/receipt.test.ts b/test/receipt.test.ts index 6e0808d..6e93333 100644 --- a/test/receipt.test.ts +++ b/test/receipt.test.ts @@ -17,7 +17,7 @@ it('issue & verify', async () => { })) - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.detached.signer({ @@ -71,7 +71,7 @@ it('issue & verify', async () => { }) it("add / remove from receipts", async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') const publicKeyJwk = await cose.key.publicFromPrivate(privateKeyJwk) const signer = cose.detached.signer({ remote: cose.crypto.signer({ @@ -103,7 +103,7 @@ it("add / remove from receipts", async () => { const receipts = await cose.receipt.get(transparentSignature) expect(receipts.length).toBe(1) // expect 1 receipt const coseKey = await cose.key.convertJsonWebKeyToCoseKey(publicKeyJwk) - coseKey.set(2, await cose.key.thumbprint.calculateCoseKeyThumbprintUri(coseKey)) + coseKey.set(cose.EC2.Kid, await cose.key.thumbprint.calculateCoseKeyThumbprintUri(coseKey)) const publicKey = cose.key.serialize(coseKey) expect(publicKey).toBeDefined(); // fs.writeFileSync('./examples/image.ckt.signature.cbor', Buffer.from(transparentSignature)) diff --git a/test/sign1.attached.test.ts b/test/sign1.attached.test.ts index 911e589..4ac94a9 100644 --- a/test/sign1.attached.test.ts +++ b/test/sign1.attached.test.ts @@ -2,7 +2,7 @@ import fs from 'fs' import * as cose from '../src' it('sign and verify', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.attached.signer({ @@ -12,7 +12,7 @@ it('sign and verify', async () => { }) const message = '💣 test ✨ mesage 🔥' const coseSign1 = await signer.sign({ - protectedHeader: new Map([[1, -7]]), + protectedHeader: new Map([[cose.Protected.Alg, cose.Signature.ES256]]), unprotectedHeader: new Map(), payload: new TextEncoder().encode(message) }) @@ -32,7 +32,7 @@ it('sign and verify', async () => { }) it('sign and verify large image from file system', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.attached.signer({ @@ -45,8 +45,8 @@ it('sign and verify large image from file system', async () => { const coseSign1 = await signer.sign({ protectedHeader: new Map([ - [1, -7], // alg ES256 - [3, "image/png"], // content_type image/png + [cose.Protected.Alg, cose.Signature.ES256], // alg ES256 + [cose.Protected.ContentType, "image/png"], // content_type image/png ]), unprotectedHeader: new Map(), payload: content diff --git a/test/sign1.detached.test.ts b/test/sign1.detached.test.ts index 5080161..ae23b1c 100644 --- a/test/sign1.detached.test.ts +++ b/test/sign1.detached.test.ts @@ -2,7 +2,7 @@ import fs from 'fs' import * as cose from '../src' it('sign and verify', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.detached.signer({ @@ -13,8 +13,9 @@ it('sign and verify', async () => { const message = '💣 test ✨ mesage 🔥' const payload = new TextEncoder().encode(message) const coseSign1 = await signer.sign({ - protectedHeader: new Map([[1, -7]]), - unprotectedHeader: new Map(), + protectedHeader: cose.ProtectedHeader([ + [cose.Protected.Alg, cose.Signature.ES256], // alg ES256 + ]), payload }) const { tag, value } = await cose.cbor.decode(coseSign1) @@ -36,7 +37,7 @@ it('sign and verify', async () => { }) it('sign and verify large image from file system', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.detached.signer({ @@ -46,11 +47,10 @@ it('sign and verify large image from file system', async () => { }) const content = fs.readFileSync('./examples/image.png') const coseSign1 = await signer.sign({ - protectedHeader: new Map([ - [1, -7], // alg ES256 - [3, "image/png"], // content_type image/png + protectedHeader: cose.ProtectedHeader([ + [cose.Protected.Alg, cose.Signature.ES256], // alg ES256 + [cose.Protected.ContentType, "image/png"], // content_type image/png ]), - unprotectedHeader: new Map(), payload: content }) diff --git a/test/signer.test.ts b/test/signer.test.ts index d340df2..1a63c37 100644 --- a/test/signer.test.ts +++ b/test/signer.test.ts @@ -3,7 +3,7 @@ import * as cose from '../src' it('sign and verify large image from file system', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') + const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') // eslint-disable-next-line @typescript-eslint/no-unused-vars const { d, ...publicKeyJwk } = privateKeyJwk const signer = cose.detached.signer({ diff --git a/test/verifiers.test.ts b/test/verifiers.test.ts index 2b3b2a3..82b0ce4 100644 --- a/test/verifiers.test.ts +++ b/test/verifiers.test.ts @@ -8,19 +8,19 @@ it('verify multiple receipts', async () => { const notary2SecretKey = await cose.key.generate('ES256', 'application/cose-key') const issuerSigner = cose.detached.signer({ remote: cose.crypto.signer({ - privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(issuerSecretKey) + privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(issuerSecretKey) }) }) const notary1Signer = cose.detached.signer({ remote: cose.crypto.signer({ - privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(notary1SecretKey) + privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(notary1SecretKey) }) }) const notary2Signer = cose.detached.signer({ remote: cose.crypto.signer({ - privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(notary2SecretKey) + privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(notary2SecretKey) }) }) const issuerCkt = await cose.key.thumbprint.calculateCoseKeyThumbprintUri(issuerSecretKey) diff --git a/test/x509.test.ts b/test/x509.test.ts index 748a7b6..a9da368 100644 --- a/test/x509.test.ts +++ b/test/x509.test.ts @@ -9,7 +9,8 @@ it('sign and verify with x5t and key resolver', async () => { iss: 'vendor.example', sub: 'vendor.example', nbf: moment().toISOString(), // now - exp: moment().add(5, 'minutes').toISOString() // in 5 minutes + exp: moment().add(5, 'minutes').toISOString(), // in 5 minutes + serial: "01" }) // { // "public": "-----BEGIN CERTIFICATE-----\nMIIBSDC...t4fdL0yLEskA7M=\n-----END CERTIFICATE-----",