Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fully specified #19

Merged
merged 5 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading