From c4234bd4afc386c1e972d6bb215622b8d27317a6 Mon Sep 17 00:00:00 2001 From: 0xZensh Date: Fri, 19 Jan 2024 20:44:18 +0800 Subject: [PATCH] feat: enhance key's kid with CBOR --- README.md | 6 ++- package.json | 16 ++++---- pnpm-lock.yaml | 90 ++++++++++++++++++++--------------------- src/aesgcm.ts | 8 ++-- src/chacha20poly1305.ts | 8 ++-- src/ecdsa.ts | 11 +++-- src/ed25519.ts | 12 +++--- src/encrypt0.test.ts | 2 +- src/encrypt0.ts | 13 +++++- src/hmac.ts | 12 ++++-- src/key.test.ts | 25 +++++++++++- src/key.ts | 13 +++++- src/mac0.test.ts | 2 +- src/mac0.ts | 13 +++++- src/map.ts | 11 +++++ src/sign1.test.ts | 2 +- src/sign1.ts | 13 +++++- src/tag.ts | 41 ++++++++++--------- 18 files changed, 189 insertions(+), 109 deletions(-) diff --git a/README.md b/README.md index af02908..d4f4ea2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ const privKey = Ed25519Key.generate() const pubKey = privKey.public() // const pubKey = Ed25519Key.fromPublic(32_bytes_public) +const externalData = utf8ToBytes('@ldclabs/cose-ts') // optional + // signing const claims = new Claims() claims.iss = 'ldclabs' @@ -69,14 +71,14 @@ claims.exp = Math.floor(Date.now() / 1000) + 3600 claims.cti = randomBytes(16) const cwtMsg = new Sign1Message(claims.toBytes()) -const cwtData = cwtMsg.toBytes(privKey, utf8ToBytes('@ldclabs/cose-ts')) +const cwtData = cwtMsg.toBytes(privKey, externalData) // const cwtDataWithTag = withCWTTag(cwtData) // verifying const cwtMsg2 = Sign1Message.fromBytes( pubKey, cwtData, // or cwtDataWithTag - utf8ToBytes('@ldclabs/cose-ts') + externalData ) const claims2 = Claims.fromBytes(cwtMsg2.payload) const validator = new Validator({ expectedIssuer: 'ldclabs' }) diff --git a/package.json b/package.json index 5ea208a..019f124 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@ldclabs/cose-ts", "type": "module", - "version": "1.0.0", + "version": "1.1.0", "author": "0xZensh ", "description": "Implemented Keys, Algorithms (RFC9053), COSE (RFC9052) and CWT (RFC8392) in TypeScript.", "license": "MIT", @@ -120,14 +120,14 @@ "test": "vitest src --coverage --run" }, "devDependencies": { - "@types/node": "^20.11.4", + "@types/node": "^20.11.5", "@typescript-eslint/eslint-plugin": "^6.19.0", "@typescript-eslint/parser": "^6.19.0", - "@vitest/coverage-v8": "^1.2.0", + "@vitest/coverage-v8": "^1.2.1", "eslint": "^8.56.0", "eslint-plugin-import": "^2.29.1", "typescript": "^5.3.3", - "vitest": "^1.2.0" + "vitest": "^1.2.1" }, "keywords": [ "cose", @@ -137,9 +137,9 @@ "RFC9053" ], "dependencies": { - "cborg": "^4.0.8", - "@noble/ciphers": "^0.4.1", - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3" + "@noble/ciphers": "=0.4.1", + "@noble/curves": "=1.3.0", + "@noble/hashes": "=1.3.3", + "cborg": "^4.0.8" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5b83ae..427746d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,13 +6,13 @@ settings: dependencies: '@noble/ciphers': - specifier: ^0.4.1 + specifier: '=0.4.1' version: 0.4.1 '@noble/curves': - specifier: ^1.3.0 + specifier: '=1.3.0' version: 1.3.0 '@noble/hashes': - specifier: ^1.3.3 + specifier: '=1.3.3' version: 1.3.3 cborg: specifier: ^4.0.8 @@ -20,8 +20,8 @@ dependencies: devDependencies: '@types/node': - specifier: ^20.11.4 - version: 20.11.4 + specifier: ^20.11.5 + version: 20.11.5 '@typescript-eslint/eslint-plugin': specifier: ^6.19.0 version: 6.19.0(@typescript-eslint/parser@6.19.0)(eslint@8.56.0)(typescript@5.3.3) @@ -29,8 +29,8 @@ devDependencies: specifier: ^6.19.0 version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) '@vitest/coverage-v8': - specifier: ^1.2.0 - version: 1.2.0(vitest@1.2.0) + specifier: ^1.2.1 + version: 1.2.1(vitest@1.2.1) eslint: specifier: ^8.56.0 version: 8.56.0 @@ -41,8 +41,8 @@ devDependencies: specifier: ^5.3.3 version: 5.3.3 vitest: - specifier: ^1.2.0 - version: 1.2.0(@types/node@20.11.4) + specifier: ^1.2.1 + version: 1.2.1(@types/node@20.11.5) packages: @@ -556,8 +556,8 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/node@20.11.4: - resolution: {integrity: sha512-6I0fMH8Aoy2lOejL3s4LhyIYX34DPwY8bl5xlNjBvUEk8OHrcuzsFt+Ied4LvJihbtXPM+8zUqdydfIti86v9g==} + /@types/node@20.11.5: + resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==} dependencies: undici-types: 5.26.5 dev: true @@ -702,8 +702,8 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitest/coverage-v8@1.2.0(vitest@1.2.0): - resolution: {integrity: sha512-YvX8ULTUm1+zkvkl14IqXYGxE1h13OXKPoDsxazARKlp4YLrP28hHEBdplaU7ZTN/Yn6zy6Z3JadWNRJwcmyrQ==} + /@vitest/coverage-v8@1.2.1(vitest@1.2.1): + resolution: {integrity: sha512-fJEhKaDwGMZtJUX7BRcGxooGwg1Hl0qt53mVup/ZJeznhvL5EodteVnb/mcByhEcvVWbK83ZF31c7nPEDi4LOQ==} peerDependencies: vitest: ^1.0.0 dependencies: @@ -720,43 +720,43 @@ packages: std-env: 3.7.0 test-exclude: 6.0.0 v8-to-istanbul: 9.2.0 - vitest: 1.2.0(@types/node@20.11.4) + vitest: 1.2.1(@types/node@20.11.5) transitivePeerDependencies: - supports-color dev: true - /@vitest/expect@1.2.0: - resolution: {integrity: sha512-H+2bHzhyvgp32o7Pgj2h9RTHN0pgYaoi26Oo3mE+dCi1PAqV31kIIVfTbqMO3Bvshd5mIrJLc73EwSRrbol9Lw==} + /@vitest/expect@1.2.1: + resolution: {integrity: sha512-/bqGXcHfyKgFWYwIgFr1QYDaR9e64pRKxgBNWNXPefPFRhgm+K3+a/dS0cUGEreWngets3dlr8w8SBRw2fCfFQ==} dependencies: - '@vitest/spy': 1.2.0 - '@vitest/utils': 1.2.0 + '@vitest/spy': 1.2.1 + '@vitest/utils': 1.2.1 chai: 4.4.1 dev: true - /@vitest/runner@1.2.0: - resolution: {integrity: sha512-vaJkDoQaNUTroT70OhM0NPznP7H3WyRwt4LvGwCVYs/llLaqhoSLnlIhUClZpbF5RgAee29KRcNz0FEhYcgxqA==} + /@vitest/runner@1.2.1: + resolution: {integrity: sha512-zc2dP5LQpzNzbpaBt7OeYAvmIsRS1KpZQw4G3WM/yqSV1cQKNKwLGmnm79GyZZjMhQGlRcSFMImLjZaUQvNVZQ==} dependencies: - '@vitest/utils': 1.2.0 + '@vitest/utils': 1.2.1 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.2.0: - resolution: {integrity: sha512-P33EE7TrVgB3HDLllrjK/GG6WSnmUtWohbwcQqmm7TAk9AVHpdgf7M3F3qRHKm6vhr7x3eGIln7VH052Smo6Kw==} + /@vitest/snapshot@1.2.1: + resolution: {integrity: sha512-Tmp/IcYEemKaqAYCS08sh0vORLJkMr0NRV76Gl8sHGxXT5151cITJCET20063wk0Yr/1koQ6dnmP6eEqezmd/Q==} dependencies: magic-string: 0.30.5 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.2.0: - resolution: {integrity: sha512-MNxSAfxUaCeowqyyGwC293yZgk7cECZU9wGb8N1pYQ0yOn/SIr8t0l9XnGRdQZvNV/ZHBYu6GO/W3tj5K3VN1Q==} + /@vitest/spy@1.2.1: + resolution: {integrity: sha512-vG3a/b7INKH7L49Lbp0IWrG6sw9j4waWAucwnksPB1r1FTJgV7nkBByd9ufzu6VWya/QTvQW4V9FShZbZIB2UQ==} dependencies: tinyspy: 2.2.0 dev: true - /@vitest/utils@1.2.0: - resolution: {integrity: sha512-FyD5bpugsXlwVpTcGLDf3wSPYy8g541fQt14qtzo8mJ4LdEpDKZ9mQy2+qdJm2TZRpjY5JLXihXCgIxiRJgi5g==} + /@vitest/utils@1.2.1: + resolution: {integrity: sha512-bsH6WVZYe/J2v3+81M5LDU8kW76xWObKIURpPrOXm2pjBniBu2MERI/XP60GpS4PHU3jyK50LUutOwrx4CyHUg==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -2406,8 +2406,8 @@ packages: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true - /tinypool@0.8.1: - resolution: {integrity: sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg==} + /tinypool@0.8.2: + resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} engines: {node: '>=14.0.0'} dev: true @@ -2539,8 +2539,8 @@ packages: convert-source-map: 2.0.0 dev: true - /vite-node@1.2.0(@types/node@20.11.4): - resolution: {integrity: sha512-ETnQTHeAbbOxl7/pyBck9oAPZZZo+kYnFt1uQDD+hPReOc+wCjXw4r4jHriBRuVDB5isHmPXxrfc1yJnfBERqg==} + /vite-node@1.2.1(@types/node@20.11.5): + resolution: {integrity: sha512-fNzHmQUSOY+y30naohBvSW7pPn/xn3Ib/uqm+5wAJQJiqQsU0NBR78XdRJb04l4bOFKjpTWld0XAfkKlrDbySg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -2548,7 +2548,7 @@ packages: debug: 4.3.4 pathe: 1.1.2 picocolors: 1.0.0 - vite: 5.0.11(@types/node@20.11.4) + vite: 5.0.11(@types/node@20.11.5) transitivePeerDependencies: - '@types/node' - less @@ -2560,7 +2560,7 @@ packages: - terser dev: true - /vite@5.0.11(@types/node@20.11.4): + /vite@5.0.11(@types/node@20.11.5): resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -2588,7 +2588,7 @@ packages: terser: optional: true dependencies: - '@types/node': 20.11.4 + '@types/node': 20.11.5 esbuild: 0.19.11 postcss: 8.4.33 rollup: 4.9.5 @@ -2596,8 +2596,8 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.2.0(@types/node@20.11.4): - resolution: {integrity: sha512-Ixs5m7BjqvLHXcibkzKRQUvD/XLw0E3rvqaCMlrm/0LMsA0309ZqYvTlPzkhh81VlEyVZXFlwWnkhb6/UMtcaQ==} + /vitest@1.2.1(@types/node@20.11.5): + resolution: {integrity: sha512-TRph8N8rnSDa5M2wKWJCMnztCZS9cDcgVTQ6tsTFTG/odHJ4l5yNVqvbeDJYJRZ6is3uxaEpFs8LL6QM+YFSdA==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -2621,12 +2621,12 @@ packages: jsdom: optional: true dependencies: - '@types/node': 20.11.4 - '@vitest/expect': 1.2.0 - '@vitest/runner': 1.2.0 - '@vitest/snapshot': 1.2.0 - '@vitest/spy': 1.2.0 - '@vitest/utils': 1.2.0 + '@types/node': 20.11.5 + '@vitest/expect': 1.2.1 + '@vitest/runner': 1.2.1 + '@vitest/snapshot': 1.2.1 + '@vitest/spy': 1.2.1 + '@vitest/utils': 1.2.1 acorn-walk: 8.3.2 cac: 6.7.14 chai: 4.4.1 @@ -2639,9 +2639,9 @@ packages: std-env: 3.7.0 strip-literal: 1.3.0 tinybench: 2.6.0 - tinypool: 0.8.1 - vite: 5.0.11(@types/node@20.11.4) - vite-node: 1.2.0(@types/node@20.11.4) + tinypool: 0.8.2 + vite: 5.0.11(@types/node@20.11.5) + vite-node: 1.2.1(@types/node@20.11.5) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/src/aesgcm.ts b/src/aesgcm.ts index c49ddc7..2545a81 100644 --- a/src/aesgcm.ts +++ b/src/aesgcm.ts @@ -5,22 +5,22 @@ import { gcm } from '@noble/ciphers/webcrypto/aes' import * as iana from './iana' import { RawMap, assertBytes } from './map' import { Key, type Encryptor } from './key' -import { utf8ToBytes, randomBytes } from './utils' +import { randomBytes } from './utils' // TODO: more checks // AesGcmKey implements content encryption algorithm AES-GCM for COSE as defined in RFC9053. // https://datatracker.ietf.org/doc/html/rfc9053#name-aes-gcm. export class AesGcmKey extends Key implements Encryptor { - static generate(alg: number, kid?: string): AesGcmKey { + static generate(alg: number, kid?: Uint8Array): AesGcmKey { return AesGcmKey.fromSecret(randomBytes(getKeySize(alg)), kid) } - static fromSecret(secret: Uint8Array, kid?: string): AesGcmKey { + static fromSecret(secret: Uint8Array, kid?: Uint8Array): AesGcmKey { const alg = getAlg(assertBytes(secret, 'secret')) const key = new AesGcmKey() key.alg = alg if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } key.setParam(iana.SymmetricKeyParameterK, secret) return key diff --git a/src/chacha20poly1305.ts b/src/chacha20poly1305.ts index 2a63ce7..4ea684b 100644 --- a/src/chacha20poly1305.ts +++ b/src/chacha20poly1305.ts @@ -5,17 +5,17 @@ import { chacha20poly1305 } from '@noble/ciphers/chacha' import * as iana from './iana' import { RawMap, assertBytes } from './map' import { Key, type Encryptor } from './key' -import { utf8ToBytes, randomBytes } from './utils' +import { randomBytes } from './utils' // TODO: more checks // ChaCha20Poly1305Key implements content encryption algorithm ChaCha20/Poly1305 for COSE as defined in RFC9053. // https://datatracker.ietf.org/doc/html/rfc9053#name-chacha20-and-poly1305. export class ChaCha20Poly1305Key extends Key implements Encryptor { - static generate(kid?: string): ChaCha20Poly1305Key { + static generate(kid?: Uint8Array): ChaCha20Poly1305Key { return ChaCha20Poly1305Key.fromSecret(randomBytes(32), kid) } - static fromSecret(secret: Uint8Array, kid?: string): ChaCha20Poly1305Key { + static fromSecret(secret: Uint8Array, kid?: Uint8Array): ChaCha20Poly1305Key { assertBytes(secret, 'secret') if (secret.length !== 32) { throw new Error( @@ -24,7 +24,7 @@ export class ChaCha20Poly1305Key extends Key implements Encryptor { } const key = new ChaCha20Poly1305Key() if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } key.setParam(iana.SymmetricKeyParameterK, secret) return key diff --git a/src/ecdsa.ts b/src/ecdsa.ts index 3ae8ae3..2014786 100644 --- a/src/ecdsa.ts +++ b/src/ecdsa.ts @@ -8,18 +8,17 @@ import { CurveFn } from '@noble/curves/abstract/weierstrass' import * as iana from './iana' import { RawMap, assertBytes } from './map' import { Key, type Signer, Verifier } from './key' -import { utf8ToBytes } from './utils' // TODO: more checks // ECDSAKey implements signature algorithm ECDSA for COSE as defined in RFC9053. // https://datatracker.ietf.org/doc/html/rfc9053#name-ecdsa. export class ECDSAKey extends Key implements Signer, Verifier { - static generate(alg: number, kid?: string): ECDSAKey { + static generate(alg: number, kid?: Uint8Array): ECDSAKey { const curve = getCurve(alg) return ECDSAKey.fromSecret(curve.utils.randomPrivateKey(), kid) } - static fromSecret(secret: Uint8Array, kid?: string): ECDSAKey { + static fromSecret(secret: Uint8Array, kid?: Uint8Array): ECDSAKey { assertBytes(secret, 'secret') const alg = getAlg(secret.length) const key = new ECDSAKey() @@ -34,12 +33,12 @@ export class ECDSAKey extends Key implements Signer, Verifier { key.setParam(iana.EC2KeyParameterD, secret) if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } return key } - static fromPublic(pubkey: Uint8Array, kid?: string): ECDSAKey { + static fromPublic(pubkey: Uint8Array, kid?: Uint8Array): ECDSAKey { assertBytes(pubkey, 'public key') if (pubkey.length < 33) { throw new Error( @@ -79,7 +78,7 @@ export class ECDSAKey extends Key implements Signer, Verifier { } if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } return key } diff --git a/src/ed25519.ts b/src/ed25519.ts index fa64e3a..ccbb403 100644 --- a/src/ed25519.ts +++ b/src/ed25519.ts @@ -5,17 +5,17 @@ import { ed25519 } from '@noble/curves/ed25519' import * as iana from './iana' import { RawMap, assertBytes } from './map' import { Key, type Signer, Verifier } from './key' -import { utf8ToBytes, randomBytes } from './utils' +import { randomBytes } from './utils' // TODO: more checks // Ed25519Key implements signature algorithm Ed25519 for COSE as defined in RFC9053. // https://datatracker.ietf.org/doc/html/rfc9053#name-edwards-curve-digital-signa. export class Ed25519Key extends Key implements Signer, Verifier { - static generate(kid?: string): Ed25519Key { + static generate(kid?: Uint8Array): Ed25519Key { return Ed25519Key.fromSecret(randomBytes(32), kid) } - static fromSecret(secret: Uint8Array, kid?: string): Ed25519Key { + static fromSecret(secret: Uint8Array, kid?: Uint8Array): Ed25519Key { assertBytes(secret, 'secret') if (secret.length !== 32) { throw new Error( @@ -26,12 +26,12 @@ export class Ed25519Key extends Key implements Signer, Verifier { const key = new Ed25519Key() key.setParam(iana.OKPKeyParameterD, secret) if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } return key } - static fromPublic(pubkey: Uint8Array, kid?: string): Ed25519Key { + static fromPublic(pubkey: Uint8Array, kid?: Uint8Array): Ed25519Key { assertBytes(pubkey, 'public key') if (pubkey.length !== 32) { throw new Error( @@ -43,7 +43,7 @@ export class Ed25519Key extends Key implements Signer, Verifier { key.setParam(iana.OKPKeyParameterX, pubkey) if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } return key } diff --git a/src/encrypt0.test.ts b/src/encrypt0.test.ts index 23c4cee..243151b 100644 --- a/src/encrypt0.test.ts +++ b/src/encrypt0.test.ts @@ -14,7 +14,7 @@ describe('Encrypt0Message Examples', () => { it('env-pass-02: Add external data', async () => { const key = AesGcmKey.fromSecret( base64ToBytes('hJtXIZ2uSN5kbQfbtTNWbg'), - 'our-secret' + utf8ToBytes('our-secret') ) assert.equal(key.alg, iana.AlgorithmA128GCM) diff --git a/src/encrypt0.ts b/src/encrypt0.ts index ddd938d..7d920a8 100644 --- a/src/encrypt0.ts +++ b/src/encrypt0.ts @@ -7,7 +7,13 @@ import { RawMap } from './map' import { Key, type Encryptor } from './key' import * as iana from './iana' import { decodeCBOR, encodeCBOR } from './utils' -import { skipTag, withTag, CwtPrefix, Encrypt0MessagePrefix } from './tag' +import { + skipTag, + withTag, + CwtPrefix, + Encrypt0MessagePrefix, + CBORSelfPrefix, +} from './tag' // Encrypt0Message represents a COSE_Encrypt0 object. // @@ -35,7 +41,10 @@ export class Encrypt0Message { coseData: Uint8Array, externalData?: Uint8Array ): Promise { - const data = skipTag(Encrypt0MessagePrefix, skipTag(CwtPrefix, coseData)) + const data = skipTag( + Encrypt0MessagePrefix, + skipTag(CwtPrefix, skipTag(CBORSelfPrefix, coseData)) + ) const [protectedBytes, unprotected, ciphertext] = decodeCBOR(data) as [ Uint8Array, diff --git a/src/hmac.ts b/src/hmac.ts index b42c68c..ea5ec52 100644 --- a/src/hmac.ts +++ b/src/hmac.ts @@ -4,18 +4,22 @@ import * as iana from './iana' import { RawMap, assertBytes } from './map' import { Key, type MACer } from './key' -import { utf8ToBytes, randomBytes } from './utils' +import { randomBytes } from './utils' import { hmac, getHash } from './hash' // TODO: more checks // HMACKey implements message authentication code algorithm HMAC for COSE as defined in RFC9053. // https://datatracker.ietf.org/doc/html/rfc9053#name-hash-based-message-authenti. export class HMACKey extends Key implements MACer { - static generate(alg: number, kid?: string): HMACKey { + static generate(alg: number, kid?: Uint8Array): HMACKey { return HMACKey.fromSecret(randomBytes(getKeySize(alg)), alg, kid) } - static fromSecret(secret: Uint8Array, alg: number, kid?: string): HMACKey { + static fromSecret( + secret: Uint8Array, + alg: number, + kid?: Uint8Array + ): HMACKey { if (assertBytes(secret, 'secret').length != getKeySize(alg)) { throw new Error( `cose-ts: HMACKey.fromSecret: secret size mismatch, expected ${getKeySize( @@ -27,7 +31,7 @@ export class HMACKey extends Key implements MACer { const key = new HMACKey() key.alg = alg if (kid) { - key.kid = utf8ToBytes(kid) + key.kid = kid } key.setParam(iana.SymmetricKeyParameterK, secret) return key diff --git a/src/key.test.ts b/src/key.test.ts index 8ed2d38..2e2a825 100644 --- a/src/key.test.ts +++ b/src/key.test.ts @@ -2,11 +2,34 @@ // See the file LICENSE for licensing terms. import { assert, describe, it } from 'vitest' -import { bytesToHex, hexToBytes, utf8ToBytes } from './utils' +import { + bytesToHex, + hexToBytes, + utf8ToBytes, + compareBytes, + encodeCBOR, +} from './utils' import * as iana from './iana' import { Key } from './key' describe('Key Examples', () => { + it('Key', () => { + const key = new Key() + key.kty = iana.KeyTypeSymmetric + key.kid = utf8ToBytes('Symmetric128') + assert.equal(compareBytes(key.kid, utf8ToBytes('Symmetric128')), 0) + key.setKid('Symmetric128') + assert.equal(key.getKid(), 'Symmetric128') + assert.equal(compareBytes(key.kid, encodeCBOR('Symmetric128')), 0) + + key.setKid(new Uint8Array([1, 2, 3, 4])) + assert.equal(compareBytes(key.getKid(), new Uint8Array([1, 2, 3, 4])), 0) + assert.equal( + compareBytes(key.kid, encodeCBOR(new Uint8Array([1, 2, 3, 4]))), + 0 + ) + }) + it('128-Bit Symmetric COSE_Key', () => { const key = new Key() key.kty = iana.KeyTypeSymmetric diff --git a/src/key.ts b/src/key.ts index e8c2eb3..9bc58b8 100644 --- a/src/key.ts +++ b/src/key.ts @@ -3,7 +3,7 @@ import { KVMap, RawMap, assertIntOrText, assertBytes } from './map' import * as iana from './iana' -import { decodeCBOR } from './utils' +import { decodeCBOR, encodeCBOR } from './utils' export interface Encryptor { nonceSize(): number @@ -86,4 +86,15 @@ export class Key extends KVMap { set baseIV(iv: Uint8Array) { this.setParam(iana.KeyParameterBaseIV, assertBytes(iv, 'Base IV')) } + + // getKid gets the kid parameter with CBOR decoding. + getKid(): T { + return decodeCBOR(this.getBytes(iana.KeyParameterKid, 'kid')) + } + + // setKid sets the kid parameter with CBOR encoding. + setKid(kid: T): this { + this.setParam(iana.KeyParameterKid, assertBytes(encodeCBOR(kid), 'kid')) + return this + } } diff --git a/src/mac0.test.ts b/src/mac0.test.ts index e2c819d..4949fc7 100644 --- a/src/mac0.test.ts +++ b/src/mac0.test.ts @@ -13,7 +13,7 @@ describe('Mac0Message Examples', () => { const key = HMACKey.fromSecret( base64ToBytes('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'), iana.AlgorithmHMAC_256_256, - 'our-secret' + utf8ToBytes('our-secret') ) const msg = new Mac0Message( utf8ToBytes('This is the content.'), diff --git a/src/mac0.ts b/src/mac0.ts index 38e8959..29c6a0c 100644 --- a/src/mac0.ts +++ b/src/mac0.ts @@ -6,7 +6,13 @@ import { RawMap } from './map' import { Key, type MACer } from './key' import * as iana from './iana' import { decodeCBOR, encodeCBOR, compareBytes } from './utils' -import { skipTag, withTag, CwtPrefix, Mac0MessagePrefix } from './tag' +import { + skipTag, + withTag, + CwtPrefix, + Mac0MessagePrefix, + CBORSelfPrefix, +} from './tag' // Mac0Message represents a COSE_Mac0 object. // @@ -36,7 +42,10 @@ export class Mac0Message { coseData: Uint8Array, externalData?: Uint8Array ): Mac0Message { - const data = skipTag(Mac0MessagePrefix, skipTag(CwtPrefix, coseData)) + const data = skipTag( + Mac0MessagePrefix, + skipTag(CwtPrefix, skipTag(CBORSelfPrefix, coseData)) + ) const [protectedBytes, unprotected, payload, tag] = decodeCBOR(data) as [ Uint8Array, diff --git a/src/map.ts b/src/map.ts index 6eb0658..7fa1555 100644 --- a/src/map.ts +++ b/src/map.ts @@ -132,6 +132,17 @@ export class KVMap { return this } + getCBORParam(key: Label): T | undefined { + return this._raw.has(key) + ? decodeCBOR(assertBytes(this._raw.get(key), String(key))) + : undefined + } + + setCBORParam(key: Label, value: T): this { + this._raw.set(key, encodeCBOR(value)) + return this + } + clone(): RawMap { return new Map(this._raw) } diff --git a/src/sign1.test.ts b/src/sign1.test.ts index 532f470..b890294 100644 --- a/src/sign1.test.ts +++ b/src/sign1.test.ts @@ -19,7 +19,7 @@ describe('Sign1Message Examples', () => { it('sign-pass-02: External', async () => { const key = ECDSAKey.fromSecret( base64ToBytes('V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM'), - '11' + utf8ToBytes('11') ) assert.equal(key.kty, iana.KeyTypeEC2) assert.equal(key.alg, iana.AlgorithmES256) diff --git a/src/sign1.ts b/src/sign1.ts index c3761bd..c0a8810 100644 --- a/src/sign1.ts +++ b/src/sign1.ts @@ -6,7 +6,13 @@ import { RawMap } from './map' import { Key, type Verifier, Signer } from './key' import * as iana from './iana' import { decodeCBOR, encodeCBOR } from './utils' -import { skipTag, withTag, CwtPrefix, Sign1MessagePrefix } from './tag' +import { + skipTag, + withTag, + CwtPrefix, + Sign1MessagePrefix, + CBORSelfPrefix, +} from './tag' // Sign1Message represents a COSE_Sign1 object. // @@ -34,7 +40,10 @@ export class Sign1Message { coseData: Uint8Array, externalData?: Uint8Array ): Sign1Message { - const data = skipTag(Sign1MessagePrefix, skipTag(CwtPrefix, coseData)) + const data = skipTag( + Sign1MessagePrefix, + skipTag(CwtPrefix, skipTag(CBORSelfPrefix, coseData)) + ) const [protectedBytes, unprotected, payload, signature] = decodeCBOR( data diff --git a/src/tag.ts b/src/tag.ts index 5ef9a3d..05f95f8 100644 --- a/src/tag.ts +++ b/src/tag.ts @@ -1,10 +1,13 @@ import { concatBytes } from './utils' -// cwtPrefix represents the fixed prefix of CWT CBOR tag. -// https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags -export const CwtPrefix = new Uint8Array([ - 0xd8, - 0x3d, // #6.61 +// encrypt0MessagePrefix represents the fixed prefix of COSE_Encrypt0_Tagged. +export const Encrypt0MessagePrefix = new Uint8Array([ + 0xd0, // #6.16 +]) + +// mac0MessagePrefix represents the fixed prefix of COSE_Mac0_Tagged. +export const Mac0MessagePrefix = new Uint8Array([ + 0xd1, // #6.17 ]) // sign1MessagePrefix represents the fixed prefix of COSE_Sign1_Tagged. @@ -12,15 +15,17 @@ export const Sign1MessagePrefix = new Uint8Array([ 0xd2, // #6.18 ]) -// signMessagePrefix represents the fixed prefix of COSE_Sign_Tagged. -export const SignMessagePrefix = new Uint8Array([ +// cwtPrefix represents the fixed prefix of CWT CBOR tag. +// https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags +export const CwtPrefix = new Uint8Array([ 0xd8, - 0x62, // #6.98 + 0x3d, // #6.61 ]) -// mac0MessagePrefix represents the fixed prefix of COSE_Mac0_Tagged. -export const Mac0MessagePrefix = new Uint8Array([ - 0xd1, // #6.17 +// encryptMessagePrefix represents the fixed prefix of COSE_Encrypt_Tagged. +export const EncryptMessagePrefix = new Uint8Array([ + 0xd8, + 0x60, // #6.96 ]) // macMessagePrefix represents the fixed prefix of COSE_Mac_Tagged. @@ -29,17 +34,15 @@ export const MacMessagePrefix = new Uint8Array([ 0x61, // #6.97 ]) -// encrypt0MessagePrefix represents the fixed prefix of COSE_Encrypt0_Tagged. -export const Encrypt0MessagePrefix = new Uint8Array([ - 0xd0, // #6.16 -]) - -// encryptMessagePrefix represents the fixed prefix of COSE_Encrypt_Tagged. -export const EncryptMessagePrefix = new Uint8Array([ +// signMessagePrefix represents the fixed prefix of COSE_Sign_Tagged. +export const SignMessagePrefix = new Uint8Array([ 0xd8, - 0x60, // #6.96 + 0x62, // #6.98 ]) +// https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor +export const CBORSelfPrefix = new Uint8Array([0xd9, 0xd9, 0xf7]) + export function withTag(tag: Uint8Array, data: Uint8Array): Uint8Array { return concatBytes(tag, data) }