Skip to content

Commit

Permalink
Merge pull request #19 from transmute-industries/fully-specified
Browse files Browse the repository at this point in the history
Fully specified
  • Loading branch information
OR13 authored Aug 24, 2024
2 parents de25eb9 + 179e27f commit 7f53723
Show file tree
Hide file tree
Showing 35 changed files with 273 additions and 146 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
35 changes: 22 additions & 13 deletions src/cose/Params.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -96,7 +98,8 @@ export const Hash = {
}

export const Signature = {
'ES256': -7
'ES256': -7,
'ES384': -35
}


Expand All @@ -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 = {
Expand Down
27 changes: 12 additions & 15 deletions src/cose/key/convertCoseKeyToJsonWebKey.ts
Original file line number Diff line number Diff line change
@@ -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 <T>(coseKey: CoseKey): Promise<T> => {
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')
}
Expand All @@ -36,9 +33,9 @@ export const convertCoseKeyToJsonWebKey = async <T>(coseKey: CoseKey): Promise<T
alg: foundAlgorithm.Name,
crv: foundCurve.Name
} as any
const x = coseKey.get(-2) as any
const y = coseKey.get(-3) as any
const d = coseKey.get(-4) as any
const x = coseKey.get(EC2.X) as any
const y = coseKey.get(EC2.Y) as any
const d = coseKey.get(EC2.D) as any
if (x) {
jwk.x = base64url.encode(x)
}
Expand Down
12 changes: 4 additions & 8 deletions src/cose/key/convertJsonWebKeyToCoseKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { base64url } from 'jose'
import { toArrayBuffer } from '../../cbor'

import { IANACOSEKeyCommonParameters } from '../key-common-parameters';
import { IANACOSEAlgorithms } from '../algorithms';
import { IANACOSEKeyTypeParameters, IANACOSEKeyTypeParameter } from '../key-type-parameters';
import { IANACOSEKeyTypes } from '../key-type';
import { IANACOSEEllipticCurves } from '../elliptic-curves';
import { PublicKeyJwk, SecretKeyJwk } from '../sign1';
import { PublicKeyJwk, PrivateKeyJwk } from '../sign1';
import { iana } from '../../iana';


const algorithms = Object.values(IANACOSEAlgorithms)
const commonParams = Object.values(IANACOSEKeyCommonParameters)
const keyTypeParams = Object.values(IANACOSEKeyTypeParameters)
const keyTypes = Object.values(IANACOSEKeyTypes)
Expand Down Expand Up @@ -40,7 +38,7 @@ const getKeyTypeSpecificLabel = (keyType: 'EC2' | 'OKP', keyTypeParam: string) =
return label
}

export const convertJsonWebKeyToCoseKey = async <T>(jwk: PublicKeyJwk | SecretKeyJwk): Promise<T> => {
export const convertJsonWebKeyToCoseKey = async <T>(jwk: PublicKeyJwk | PrivateKeyJwk): Promise<T> => {

const { kty } = jwk
let coseKty = `${kty}` as 'OKP' | 'EC' | 'EC2'; // evidence of terrible design.
Expand Down Expand Up @@ -82,9 +80,7 @@ export const convertJsonWebKeyToCoseKey = async <T>(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 {
Expand Down
17 changes: 13 additions & 4 deletions src/cose/key/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,17 +19,25 @@ import { thumbprint } from "./thumbprint"

import { formatJwk } from './formatJwk'

import { iana } from '../../iana'
import { Key } from "../Params"

export const generate = async <T>(alg: CoseSignatureAlgorithms, contentType: PrivateKeyContentType = 'application/jwk+json'): Promise<T> => {
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
Expand All @@ -40,7 +49,7 @@ export const generate = async <T>(alg: CoseSignatureAlgorithms, contentType: Pri
delete privateKeyJwk.kid;
const secretKeyCoseKey = await convertJsonWebKeyToCoseKey<CoseKey>(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.')
Expand Down
4 changes: 2 additions & 2 deletions src/cose/key/index.ts
Original file line number Diff line number Diff line change
@@ -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<CoseMapKey, unknown>
Expand Down
17 changes: 9 additions & 8 deletions src/cose/key/publicFromPrivate.ts
Original file line number Diff line number Diff line change
@@ -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')
}
Expand All @@ -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 = <T>(secretKey: SecretKeyJwk | CoseKey) => {
if ((secretKey as any).kty) {
return extracePublicKeyJwk(secretKey as SecretKeyJwk) as T
export const publicFromPrivate = <T>(secretKey: PrivateKeyJwk | CoseKey) => {
if ((secretKey as PrivateKeyJwk).kty) {
return extractPublicKeyJwk(secretKey as PrivateKeyJwk) as T
}
return extractPublicCoseKey(secretKey as CoseKey) as T
}
2 changes: 1 addition & 1 deletion src/cose/key/serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CoseKey } from '.'

export const serialize = <T>(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)
Expand Down
13 changes: 9 additions & 4 deletions src/cose/key/thumbprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any, any>): Promise<ArrayBuffer> => {
const calculateCoseKeyThumbprint = async (coseKey: CoseKey): Promise<ArrayBuffer> => {
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)
}
}
Expand All @@ -19,7 +24,7 @@ const calculateCoseKeyThumbprint = async (coseKey: Map<any, any>): Promise<Array
return digest
}

const calculateCoseKeyThumbprintUri = async (coseKey: Map<any, any>): Promise<string> => {
const calculateCoseKeyThumbprintUri = async (coseKey: CoseKey): Promise<string> => {
const prefix = `urn:ietf:params:oauth:ckt:sha-256`
const digest = await calculateCoseKeyThumbprint(coseKey)
return `${prefix}:${base64url.encode(new Uint8Array(digest))}`
Expand Down
5 changes: 3 additions & 2 deletions src/cose/receipt/add.ts
Original file line number Diff line number Diff line change
@@ -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<ArrayBuffer> => {
Expand All @@ -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 }));
}
6 changes: 3 additions & 3 deletions src/cose/receipt/consistency/issue.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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')
}

Expand All @@ -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')
}

Expand Down
4 changes: 2 additions & 2 deletions src/cose/receipt/consistency/verify.ts
Original file line number Diff line number Diff line change
@@ -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"

Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion src/cose/receipt/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { decodeFirstSync, Sign1Tag } from '../../cbor'
import { Receipts } from '../Params';
import { CoseSign1Bytes } from "../sign1";

export const get = async (signature: CoseSign1Bytes): Promise<CoseSign1Bytes[]> => {
Expand All @@ -10,6 +11,6 @@ export const get = async (signature: CoseSign1Bytes): Promise<CoseSign1Bytes[]>
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
}
Loading

0 comments on commit 7f53723

Please sign in to comment.