From 4ab784169cf3bad6ffb6e5b51d3fd4aa92d6ca4a Mon Sep 17 00:00:00 2001 From: Glenn Gore Date: Thu, 12 Nov 2026 22:04:56 +1000 Subject: [PATCH] chore(core): @openvtc/vti-didcomm-js 0.16; vm-key.ts delegates to the library 0.16 ships vti-didcomm-js #38 (did:webs resolution), #39 (the verification-method module this package carried a port of) and #37 (didwebvh-ts 3). vm-key.ts keeps its TypeScript surface and calls @openvtc/vti-didcomm-js/verification-method, so there is one implementation of the key-selection rules (R4.1). Signed-off-by: Glenn Gore --- package-lock.json | 71 +++++----- packages/core/CHANGELOG.md | 10 ++ packages/core/package.json | 2 +- packages/core/src/did/vm-key.ts | 225 ++++---------------------------- 4 files changed, 72 insertions(+), 236 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e3a21b..2576d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2349,13 +2349,14 @@ } }, "node_modules/@openvtc/vti-didcomm-js": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@openvtc/vti-didcomm-js/-/vti-didcomm-js-0.14.0.tgz", - "integrity": "sha512-UZMbK3csThRurBgWj4tFYMYVRMZAIhv+MJDzkGVLjZJBl726O2BL0JSZDDBGudW3kzXOqH33+7tkiNNFJO/gnQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@openvtc/vti-didcomm-js/-/vti-didcomm-js-0.16.0.tgz", + "integrity": "sha512-M7CkmJgcvVRpgzO0UOYRfCbSdaAqfqcN9I9NamzkYsRKKXqxzufycp8+WOLdFIZcXhnrRO7j62/uCxBJOKo6hg==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^2.4.0", - "didwebvh-ts": "^2.8.0" + "@noble/hashes": "^2.4.0", + "didwebvh-ts": "^3.0.0" }, "engines": { "node": ">=20.0.0" @@ -3978,6 +3979,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { @@ -4108,6 +4110,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, "license": "MIT", "engines": { "node": "18 || 20 || >=22" @@ -4130,6 +4133,7 @@ "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -4508,20 +4512,29 @@ "node": ">=8" } }, + "node_modules/did-resolver": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/did-resolver/-/did-resolver-6.0.0.tgz", + "integrity": "sha512-qYlNpucsgfBE0foIqlnEjLx7ZvMTUOkPeIjOhVKPJKm4h8afFNS78zDLbiJPZ2yvY3Z7C79FW1UHSRyehRCpOg==", + "license": "Apache-2.0" + }, "node_modules/didwebvh-ts": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/didwebvh-ts/-/didwebvh-ts-2.8.0.tgz", - "integrity": "sha512-JAIK/Udr7xbGoYTDRjbm34nD8iddREIQffRL+vsrCaREyVtZDuy8SdFs5lXoop9zwEjXwh64ZFRSOYm6ZCZcAw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/didwebvh-ts/-/didwebvh-ts-3.1.0.tgz", + "integrity": "sha512-nStqzKdFB/D7wQpFoS9luTM9HhZhrwyjVd7fK9HGFKep3pcMU8rctyljBKJqZGxu/Xz+ziR+Wg5FnKtJGKkxyA==", "license": "Apache-2.0", "dependencies": { - "@noble/hashes": "^2.2.0", - "cookie": "^1.1.1", - "glob": "^13.0.6", - "js-yaml": "^4.1.1", - "json-canonicalize": "^2.0.0" + "@noble/curves": "^2.4.0", + "@noble/hashes": "^2.4.0", + "@scure/base": "^2.4.0", + "did-resolver": "^6.0.0", + "json-canonicalize": "^3.0.1" }, "bin": { "didwebvh": "dist/cli/didwebvh.js" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/dunder-proto": { @@ -5096,23 +5109,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -5772,6 +5768,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, "funding": [ { "type": "github", @@ -5804,14 +5801,12 @@ } }, "node_modules/json-canonicalize": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/json-canonicalize/-/json-canonicalize-2.0.1.tgz", - "integrity": "sha512-TuGrLM400c1VN8bQsSOiyDEEmRjIVgFvcSehWC+0JJaV4BGl1vYNxHBPVsN5ADUZIcWp75IN1yf+vCZyDjPt1Q==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-canonicalize/-/json-canonicalize-3.0.1.tgz", + "integrity": "sha512-xEzkTucfHhDooWOSMOvrlll9oTk6/mXZIerF2DFWA2chHZsnc2zrmAKjVOyDbSYHr0e3Djx9F89nZ3U2Bn+lSw==", "license": "MIT", "engines": { - "node": ">=8.5", - "npm": ">=6.0", - "yarn": "^1.0" + "node": ">=8.5" } }, "node_modules/json-schema-to-typescript": { @@ -6170,6 +6165,7 @@ "version": "11.5.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -6214,6 +6210,7 @@ "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "brace-expansion": "^5.0.8" @@ -6239,6 +6236,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" @@ -6386,6 +6384,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -8104,7 +8103,7 @@ "@cfworker/json-schema": "^4.1.4", "@noble/curves": "^2.4.0", "@openvtc/trust-tasks": "^0.28.8", - "@openvtc/vti-didcomm-js": "^0.14.0", + "@openvtc/vti-didcomm-js": "^0.16.0", "@openvtc/vti-tsp-js": "^0.4.0", "@scure/base": "^2.2.0", "cbor-x": "^1.6.8" diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 09280d8..2597965 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -8,6 +8,16 @@ For history before this file, see `git log` on `packages/core`. ## [Unreleased] +### Changed + +- **`@openvtc/vti-didcomm-js` 0.16** (from 0.14). The default resolver + resolves did:webs (vti-didcomm-js #38), and DIDComm/TSP key agreement picks + the X25519 `keyAgreement` key past a P-256 one (#39). `did/vm-key.ts` now + delegates to the library's `verification-method` export rather than + carrying its own port of it (R4.1). did:webvh verification moves to + `didwebvh-ts` 3, which refuses a did:webvh whose host is `localhost`: a + local webvh server needs a fully qualified host name. + ### Added - **Checking a vote after close** (VTI-VOT-053). `pcsVoterReceiptsList` diff --git a/packages/core/package.json b/packages/core/package.json index d6d18ca..f7459f4 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -153,7 +153,7 @@ "@cfworker/json-schema": "^4.1.4", "@noble/curves": "^2.4.0", "@openvtc/trust-tasks": "^0.28.8", - "@openvtc/vti-didcomm-js": "^0.14.0", + "@openvtc/vti-didcomm-js": "^0.16.0", "@openvtc/vti-tsp-js": "^0.4.0", "@scure/base": "^2.2.0", "cbor-x": "^1.6.8" diff --git a/packages/core/src/did/vm-key.ts b/packages/core/src/did/vm-key.ts index a5fb998..4082f3d 100644 --- a/packages/core/src/did/vm-key.ts +++ b/packages/core/src/did/vm-key.ts @@ -1,49 +1,25 @@ // DID document verification methods → key bytes, chosen by relationship. // -// TO BE REPLACED by `@openvtc/vti-didcomm-js/verification-method` (its -// `publicKeyOf`, `selectKey`, `x25519KeyAgreement`) when this package moves to -// the vti-didcomm-js release that ships it (vti-didcomm-js #39). This is a port -// of that module, rule for rule and with the same signatures, so the swap is -// an import change and a deletion — not a second implementation to keep in -// step (R4.1: don't fork fixes). Change the library first; mirror here only -// until the bump. +// The rules live in `@openvtc/vti-didcomm-js/verification-method` (since +// 0.15.0); this module only gives them this package's TypeScript types, so +// there is one implementation to keep right (R4.1: don't fork fixes). Change +// the library, never this file, to change what is accepted. // -// The one place this package turns a verification method into a public key. // Every site that reads a key out of a DID document — Trust-Task proof // verification, the TSP VID signing key, a member's directory-card keys, a // Domain Linkage Credential's key — goes through here, so a DID method that -// publishes its keys differently is supported everywhere at once or nowhere. -// -// Two encodings are read: -// -// - `publicKeyMultibase` (multicodec multikey) — did:key, did:peer, -// did:webvh, did:scid. Types `Multikey`, `Ed25519VerificationKey2020`, -// `X25519KeyAgreementKey2020`, `X25519KeyAgreementKey2019`. -// - `publicKeyJwk` — did:webs (`JsonWebKey`, ids `#`), and -// `JsonWebKey2020`. OKP Ed25519 / X25519 and EC P-256. -// -// Anything else fails closed: an unknown or missing `type`, a type whose -// encoding the method does not carry (a `JsonWebKey` with only -// `publicKeyMultibase`), both encodings at once, a curve there is no primitive -// for (secp256k1, P-384), a curve that contradicts the type, a JWK carrying -// private material, or a method marked `revoked`. -// -// A key is chosen by its verification relationship (`keyAgreement`, -// `authentication`, `assertionMethod`, …), never as "the first key of the -// right curve" in `verificationMethod[]` and never by a fragment suffix. Every -// party must be able to resolve a did:webs (VTI-KEY-003), and a did:webs with -// key roles (VTI-KEY-150 / VTI-KEY-173) binds different keys to each -// relationship: a key listed under one is not authorised for another. Entries -// are references (absolute, or relative to the DID) into -// `verificationMethod[]`, or embedded methods, as DID Core allows. +// publishes its keys differently (did:webs's `publicKeyJwk`, VTI-KEY-003) is +// supported everywhere at once or nowhere. A key is chosen by its +// verification relationship, never as "the first key of the right curve" and +// never by a fragment suffix; anything the library cannot read unambiguously +// fails closed. -import { p256 } from "@noble/curves/nist.js"; -import { jwk as jwkLib, multibase, sameReference } from "@openvtc/vti-didcomm-js"; +import * as lib from "@openvtc/vti-didcomm-js/verification-method"; /** Curves there is a primitive for. */ -export const ED25519 = "Ed25519"; -export const X25519 = "X25519"; -export const P256 = "P-256"; +export const ED25519 = lib.ED25519; +export const X25519 = lib.X25519; +export const P256 = lib.P256; export type KeyCurve = typeof ED25519 | typeof X25519 | typeof P256; @@ -68,25 +44,6 @@ export function isVerificationRelationship(value: unknown): value is Verificatio return typeof value === "string" && (VERIFICATION_RELATIONSHIPS as readonly string[]).includes(value); } -/** Verification method types that carry `publicKeyMultibase`, and the curve each pins (null: any). */ -const MULTIKEY_TYPES = new Map([ - ["Multikey", null], - ["Ed25519VerificationKey2020", ED25519], - ["X25519KeyAgreementKey2020", X25519], - ["X25519KeyAgreementKey2019", X25519], -]); - -/** Verification method types that carry `publicKeyJwk`. */ -const JWK_TYPES = new Set(["JsonWebKey", "JsonWebKey2020"]); - -const CODECS: ReadonlyArray = [ - [multibase.MULTICODEC.ED25519_PUB, ED25519], - [multibase.MULTICODEC.X25519_PUB, X25519], - [multibase.MULTICODEC.P256_PUB, P256], -]; - -const PREFIX = "verification-method: "; - /** A public key read from a verification method. `publicKey` is 32 raw bytes * for Ed25519 / X25519 and the 65-byte uncompressed SEC1 point for P-256. */ export interface VerificationMethodKey { @@ -112,48 +69,19 @@ export interface SelectKeyOptions { /** * The public key a verification method carries. * - * @throws {Error} on anything it cannot read unambiguously (see the module - * documentation). + * @throws {Error} on anything it cannot read unambiguously. */ export function publicKeyOf(vm: unknown): VerificationMethodKey { - if (!vm || typeof vm !== "object" || Array.isArray(vm)) { - throw new TypeError(`${PREFIX}a verification method must be an object`); - } - const m = vm as Record; - const label = typeof m.id === "string" ? m.id : "(no id)"; - if (m.revoked !== undefined && m.revoked !== null) { - throw new Error(`${PREFIX}${label} is revoked`); - } - const hasMultibase = m.publicKeyMultibase !== undefined; - const hasJwk = m.publicKeyJwk !== undefined; - if (hasMultibase && hasJwk) { - throw new Error(`${PREFIX}${label} carries both publicKeyMultibase and publicKeyJwk`); - } - const type = m.type; - if (typeof type === "string" && MULTIKEY_TYPES.has(type)) { - if (!hasMultibase) throw new Error(`${PREFIX}${label} is a ${type} without publicKeyMultibase`); - const key = fromMultikey(m.publicKeyMultibase, label); - const pinned = MULTIKEY_TYPES.get(type); - if (pinned && key.crv !== pinned) { - throw new Error(`${PREFIX}${label} is a ${type} holding a ${key.crv} key`); - } - return key; - } - if (typeof type === "string" && JWK_TYPES.has(type)) { - if (!hasJwk) throw new Error(`${PREFIX}${label} is a ${type} without publicKeyJwk`); - return fromJwk(m.publicKeyJwk, label); - } - throw new Error(`${PREFIX}${label} has unsupported type ${JSON.stringify(type)}`); + return lib.publicKeyOf(vm as object); } /** * Choose a key from `relationship` of a DID document. * - * With `kid`, the entry whose id is exactly `kid` (a did:scid `src` aside), - * which must be readable and of one of `curves`; it is refused if it is listed - * only under another relationship. Without `kid`, the first entry that is - * readable and of one of `curves`, in document order — an entry of another - * curve or of a type that cannot be read is passed over, never used. + * With `kid`, the entry whose id is exactly `kid`, which must be readable and + * of one of `curves`; it is refused if it is listed only under another + * relationship. Without `kid`, the first entry that is readable and of one of + * `curves`, in document order. * * @param did - the DID the document was resolved for; the base of relative ids. * @throws {Error} when no entry qualifies. @@ -164,14 +92,12 @@ export function selectKey( relationship: VerificationRelationship, { kid, curves, label }: SelectKeyOptions = {}, ): SelectedKey { - try { - return select(didDocument, did, relationship, kid, curves); - } catch (e) { - if (label && e instanceof Error && e.message.startsWith(PREFIX)) { - e.message = `${label}: ${e.message.slice(PREFIX.length)}`; - } - throw e; - } + const key = lib.selectKey(didDocument as object, did, relationship, { + ...(kid !== undefined ? { kid } : {}), + ...(curves !== undefined ? { curves: [...curves] } : {}), + ...(label !== undefined ? { label } : {}), + }); + return { ...key, publicJwk: key.publicJwk as Record }; } /** The X25519 `keyAgreement` key of a DID document — or the one whose id is `kid`. */ @@ -180,107 +106,8 @@ export function x25519KeyAgreement( did: string, { kid, label }: { kid?: string; label?: string } = {}, ): { kid: string; x25519Pub: Uint8Array } { - const key = selectKey(didDocument, did, "keyAgreement", { + return lib.x25519KeyAgreement(didDocument as object, did, { ...(kid !== undefined ? { kid } : {}), - curves: [X25519], ...(label !== undefined ? { label } : {}), }); - return { kid: key.kid, x25519Pub: key.publicKey }; -} - -// ─── Internals ────────────────────────────────────────────────────────── - -function select( - didDocument: unknown, - did: string, - relationship: VerificationRelationship, - kid: string | undefined, - curves: readonly KeyCurve[] | undefined, -): SelectedKey { - if (!didDocument || typeof didDocument !== "object") { - throw new TypeError(`${PREFIX}no DID document for ${did}`); - } - const doc = didDocument as Record; - const entries = doc[relationship]; - if (!Array.isArray(entries) || entries.length === 0) { - throw new Error(`${PREFIX}${did} has no ${relationship} entries`); - } - const allowed = curves ? new Set(curves) : null; - const methods: unknown[] = Array.isArray(doc.verificationMethod) ? doc.verificationMethod : []; - const abs = (id: unknown): unknown => (typeof id === "string" && id.startsWith("#") ? `${did}${id}` : id); - const idOf = (v: unknown): unknown => (v && typeof v === "object" ? (v as { id?: unknown }).id : undefined); - const reasons: string[] = []; - for (const entry of entries) { - const id = abs(typeof entry === "string" ? entry : idOf(entry)); - if (typeof id !== "string") { - reasons.push("an entry without an id"); - continue; - } - if (kid && !sameReference(id, kid)) continue; - const vm = typeof entry === "string" ? methods.find((v) => sameReference(abs(idOf(v)) as string, id)) : entry; - let key: VerificationMethodKey; - try { - if (!vm) throw new Error(`${PREFIX}${relationship} reference ${id} is not in verificationMethod[]`); - key = publicKeyOf(vm); - if (allowed && !allowed.has(key.crv)) { - throw new Error(`${PREFIX}${id} is ${key.crv}, not ${[...allowed].join(" or ")}`); - } - } catch (e) { - // An explicitly named key is used or refused, never substituted. - if (kid) throw e; - reasons.push(e instanceof Error ? e.message : String(e)); - continue; - } - return { - kid: kid ?? id, - crv: key.crv, - publicKey: key.publicKey, - publicJwk: jwkLib.publicJwk(key.crv, key.publicKey) as unknown as Record, - }; - } - if (kid) throw new Error(`${PREFIX}${did} lists no ${relationship} key ${kid}`); - const wanted = allowed ? ` ${[...allowed].join("/")}` : ""; - throw new Error( - `${PREFIX}${did} has no usable${wanted} ${relationship} key` + (reasons.length ? ` (${reasons.join("; ")})` : ""), - ); -} - -function fromMultikey(value: unknown, label: string): VerificationMethodKey { - if (typeof value !== "string") { - throw new Error(`${PREFIX}${label} publicKeyMultibase must be a string`); - } - const { codec, key } = multibase.decodeMultikey(value); - const match = CODECS.find(([c]) => c.length === codec.length && c.every((b, i) => b === codec[i])); - if (!match) { - const hex = Array.from(codec, (b) => b.toString(16).padStart(2, "0")).join(""); - throw new Error(`${PREFIX}${label} uses unsupported multicodec 0x${hex}`); - } - const crv = match[1]; - if (crv === P256) { - if (key.length !== 33 && key.length !== 65) { - throw new Error(`${PREFIX}${label} P-256 key must be a 33- or 65-byte SEC1 point`); - } - // vti-didcomm-js #39 adds `p256.uncompressedPoint` for this; 0.14 lacks it. - return { crv, publicKey: p256.Point.fromBytes(key).toBytes(false) }; - } - if (key.length !== 32) { - throw new Error(`${PREFIX}${label} ${crv} key must be 32 bytes, got ${key.length}`); - } - return { crv, publicKey: key }; -} - -function fromJwk(value: unknown, label: string): VerificationMethodKey { - if (!value || typeof value !== "object" || Array.isArray(value)) { - throw new Error(`${PREFIX}${label} publicKeyJwk must be an object`); - } - if ((value as { d?: unknown }).d !== undefined) { - throw new Error(`${PREFIX}${label} publicKeyJwk carries private key material`); - } - try { - const jwk = value as Parameters[0]; - const crv = jwkLib.curveOf(jwk) as KeyCurve; - return { crv, publicKey: jwkLib.rawPublic(jwk) }; - } catch (e) { - throw new Error(`${PREFIX}${label} publicKeyJwk: ${e instanceof Error ? e.message : String(e)}`); - } }