diff --git a/src/cose/Params.ts b/src/cose/Params.ts index f4d6596..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 @@ -116,28 +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, 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 | PrivateK } 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..26d85fd 100644 --- a/src/cose/key/generate.ts +++ b/src/cose/key/generate.ts @@ -7,7 +7,7 @@ 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' export type ContentTypeOfJsonWebKey = 'application/jwk+json' export type ContentTypeOfCoseKey = 'application/cose-key' export type PrivateKeyContentType = ContentTypeOfCoseKey | ContentTypeOfJsonWebKey @@ -18,17 +18,24 @@ import { thumbprint } from "./thumbprint" import { formatJwk } from './formatJwk' +import { iana } from '../../iana' 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 diff --git a/src/cose/key/publicFromPrivate.ts b/src/cose/key/publicFromPrivate.ts index 5b8245d..63eaf2d 100644 --- a/src/cose/key/publicFromPrivate.ts +++ b/src/cose/key/publicFromPrivate.ts @@ -1,4 +1,5 @@ import { CoseKey } from "."; +import { EC2, Key, KeyTypes } from "../Params"; import { PrivateKeyJwk } from "../sign1"; @@ -13,13 +14,13 @@ export const extractPublicKeyJwk = (privateKeyJwk: PrivateKeyJwk) => { 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 } 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/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..9e20067 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/verifier.ts b/src/cose/sign1/verifier.ts index e330cba..8aa3fc8 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; @@ -21,7 +24,7 @@ const verifier = ({ resolver }: RequestCoseSign1Verifier) => { } 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 5010fb2..c131cef 100644 --- a/src/crypto/signer.ts +++ b/src/crypto/signer.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { toArrayBuffer } from '../cbor' import { PrivateKeyJwk } from '../cose/sign1' @@ -8,15 +9,16 @@ import getDigestFromVerificationKey from '../cose/sign1/getDigestFromVerificatio 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 e1a24e8..b87cded 100644 --- a/src/x509/certificate.ts +++ b/src/x509/certificate.ts @@ -29,6 +29,11 @@ const provide = async () => { const algTowebCryptoParams: Record = { + 'ESP256': { + name: "ECDSA", + hash: "SHA-256", + namedCurve: "P-256", + }, 'ES256': { name: "ECDSA", hash: "SHA-256", diff --git a/test/fully-specified.test.ts b/test/fully-specified.test.ts index 12a2259..21421cb 100644 --- a/test/fully-specified.test.ts +++ b/test/fully-specified.test.ts @@ -1,19 +1,16 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import fs from 'fs' -import * as cose from '../src' -const message = '💣 test ✨ mesage 🔥' +import * as cose from '../src' +import { CoseSignatureAlgorithms } from '../src/cose/key' -// https://datatracker.ietf.org/doc/draft-ietf-jose-fully-specified-algorithms/ +const message = '💣 test ✨ mesage 🔥' -it('sign and verify', async () => { - const privateKeyJwk = await cose.key.generate('ES256', 'application/jwk+json') - const publicKeyJwk = await cose.key.extractPublicKeyJwk(privateKeyJwk) +const helpTestSignAndVerify = async (privateKey: cose.key.CoseKey) => { + const publicKey = await cose.key.extractPublicCoseKey(privateKey) expect(new TextDecoder().decode(await cose.attached .verifier({ resolver: { resolve: async () => { - return publicKeyJwk + return cose.key.convertCoseKeyToJsonWebKey(publicKey) } } }) @@ -21,13 +18,27 @@ it('sign and verify', async () => { coseSign1: await cose.attached .signer({ remote: cose.crypto.signer({ - privateKeyJwk + privateKeyJwk: await cose.key.convertCoseKeyToJsonWebKey(privateKey) }) }) .sign({ - protectedHeader: new Map([[1, -7]]), - unprotectedHeader: new Map(), + 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 03ab216..3fec1a9 100644 --- a/test/key.test.ts +++ b/test/key.test.ts @@ -5,9 +5,9 @@ import * as transmute from '../src' it('generate cose key', async () => { 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 + 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) @@ -52,7 +52,7 @@ it('public from private for JWK and cose key', async () => { 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/receipt.test.ts b/test/receipt.test.ts index 4fdc2ee..6e93333 100644 --- a/test/receipt.test.ts +++ b/test/receipt.test.ts @@ -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 eceb982..4ac94a9 100644 --- a/test/sign1.attached.test.ts +++ b/test/sign1.attached.test.ts @@ -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) }) @@ -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 61f128e..ae23b1c 100644 --- a/test/sign1.detached.test.ts +++ b/test/sign1.detached.test.ts @@ -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) @@ -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 })