Spaces:
Running
Running
File size: 13,726 Bytes
518343a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 | /**
* wrap_dsse.ts — Wrap SZL DSSE receipts in W3C Verifiable Credential v2 format
*
* Implements W3C VC Data Model v2.0 (https://www.w3.org/TR/vc-data-model-2.0/)
* producing JSON-LD verifiable credentials from SZL DSSE envelopes.
*
* The VC wrapper enables:
* - EU eIDAS 2.0 wallet compatibility (EUDIW ARF §6.6)
* - IETF SCITT receipt federation (draft-ietf-scitt-architecture)
* - US DoD CMMC attestation chain integration
* - Semantic web querying via SPARQL / JSON-LD framing
*
* Credential structure (per W3C VC 2.0):
* {
* "@context": ["https://www.w3.org/ns/credentials/v2", "<szl-ctx>"],
* "type": ["VerifiableCredential", "SZLGovernanceReceipt"],
* "id": "urn:szl:receipt:<receiptId>",
* "issuer": { "id": "did:web:szl.io", "name": "SZL Holdings" },
* "validFrom": "<ISO-8601>",
* "credentialSubject": { ... receipt claims ... },
* "proof": { "type": "DataIntegrityProof", ... }
* }
*
* The `proof` field uses the `ecdsa-rdfc-2022` cryptosuite when a signing key
* is available, or `DSSESignature2024` (SZL-custom) when wrapping HMAC DSSE.
*
* Refs:
* - W3C VC 2.0: https://www.w3.org/TR/vc-data-model-2.0/
* - JSON-LD 1.1: https://www.w3.org/TR/json-ld11/
* - IETF SCITT: https://datatracker.ietf.org/wg/scitt/documents/
* - eIDAS 2.0 ARF: https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework
* - IETF RFC 3986 (URI): https://www.rfc-editor.org/rfc/rfc3986
*/
import { createHash, createSign, generateKeyPairSync } from "node:crypto";
import type { DSSEEnvelope } from "../sigstore/rekor_submit.js";
import type { RekorSubmitResult } from "../sigstore/rekor_submit.js";
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
/** W3C VC 2.0 DataIntegrityProof (https://www.w3.org/TR/vc-data-model-2.0/#defn-proof) */
export interface DataIntegrityProof {
type: "DataIntegrityProof";
cryptosuite: "ecdsa-rdfc-2022" | "DSSESignature2024" | "eddsa-rdfc-2022";
created: string; // ISO-8601
verificationMethod: string; // DID URL, e.g. did:web:szl.io#key-1
proofPurpose: "assertionMethod" | "authentication" | "capabilityDelegation";
proofValue: string; // base58btc-encoded signature (or base64url for DSSE)
challenge?: string; // optional WebAuthn/SCITT challenge nonce
}
/** SZL DSSE receipt claims (the `credentialSubject` content) */
export interface SZLReceiptClaims {
id?: string; // DID or URN of the entity that performed the action
organId: string;
action: string;
outcome?: string;
operatorDID?: string;
policyRef?: string;
payloadHash: string;
governancePolicyVersion?: string;
doctrineVersion?: number;
zenodoDOI?: string;
dssePayloadType: string;
dsseSignatures: Array<{ sig: string; keyid?: string }>;
rekorAttestation?: Partial<RekorSubmitResult>;
ipfsCID?: string;
webAuthnCredentialId?: string;
}
/** Full W3C VC 2.0 verifiable credential */
export interface SZLVerifiableCredential {
"@context": [
"https://www.w3.org/ns/credentials/v2",
string // SZL context URL or inline object
];
type: ["VerifiableCredential", "SZLGovernanceReceipt", ...string[]];
id: string; // urn:szl:receipt:<receiptId>
issuer: {
id: string; // did:web:szl.io
name: string;
};
validFrom: string; // ISO-8601
validUntil?: string; // optional expiry
credentialSubject: SZLReceiptClaims;
proof: DataIntegrityProof;
}
/** Options for wrapping a DSSE receipt */
export interface WrapDSSEOptions {
/** Issuer DID (default: did:web:szl.io) */
issuerDID?: string;
/** Verification method DID URL (default: did:web:szl.io#key-1) */
verificationMethod?: string;
/** PEM private key for ecdsa-rdfc-2022 proof (dev mode).
* Leave undefined to use DSSESignature2024 (embeds DSSE sig as-is) */
signingKeyPem?: string;
/** Additional VC types beyond the defaults */
additionalTypes?: string[];
/** ISO-8601 expiry date */
validUntil?: string;
}
/** SZL receipt as parsed from a JSONL chain line */
export interface SZLReceiptRecord {
receiptId: string;
timestamp: string;
organId: string;
action: string;
outcome?: string;
operatorDID?: string;
policyRef?: string;
envelope: DSSEEnvelope;
rekorAttestation?: Partial<RekorSubmitResult>;
ipfsCID?: string;
webAuthnCredentialId?: string;
[key: string]: unknown;
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const SZL_CONTEXT_URL = "https://szl.io/ns/receipt/v1/context.json";
const DEFAULT_ISSUER_DID = "did:web:szl.io";
const DEFAULT_VERIFICATION_METHOD = "did:web:szl.io#key-1";
function decodeBase64url(b64: string): Buffer {
return Buffer.from(b64.replace(/-/g, "+").replace(/_/g, "/"), "base64");
}
function sha256Hex(data: Buffer | string): string {
return createHash("sha256").update(data).digest("hex");
}
/**
* Compute a deterministic credential ID from the receipt ID.
* Format: urn:szl:receipt:<receiptId>
* This is a URI as required by W3C VC 2.0 §4.3
*/
function makeCredentialId(receiptId: string): string {
const safe = encodeURIComponent(receiptId);
return `urn:szl:receipt:${safe}`;
}
/**
* Sign the canonical VC bytes (minus proof) using ECDSA-P256-SHA256.
* Returns a base58btc-encoded signature as required by ecdsa-rdfc-2022.
*
* Note: Full ecdsa-rdfc-2022 requires RDF Dataset Normalization (RDNA),
* which requires the `rdf-canonize` library. We approximate here with
* JSON Canonicalization Scheme (RFC 8785) for the dev path.
* STAGED-ADVISORY: Use `@digitalbazaar/ecdsa-rdfc-2022-cryptosuite` in prod.
*/
function signVCBytes(bytes: Buffer, privateKeyPem: string): string {
const signer = createSign("SHA256");
signer.update(bytes);
signer.end();
const derBytes = signer.sign(privateKeyPem);
// ecdsa-rdfc-2022 uses base58btc multibase prefix 'z'
return "z" + derBytes.toString("base64url");
}
/**
* Canonical JSON of the unsigned credential (RFC 8785 approximation).
* In production, use JSON-LD RDF Normalization (URDNA2015) per the
* ecdsa-rdfc-2022 cryptosuite spec.
*/
function canonicalizeVC(vc: Omit<SZLVerifiableCredential, "proof">): Buffer {
// RFC 8785 JCS: sorted keys, no whitespace
return Buffer.from(JSON.stringify(sortKeys(vc)));
}
function sortKeys(obj: unknown): unknown {
if (Array.isArray(obj)) return obj.map(sortKeys);
if (obj !== null && typeof obj === "object") {
return Object.keys(obj as object)
.sort()
.reduce((acc, k) => {
(acc as Record<string, unknown>)[k] = sortKeys((obj as Record<string, unknown>)[k]);
return acc;
}, {} as Record<string, unknown>);
}
return obj;
}
// ---------------------------------------------------------------------------
// Core: wrap
// ---------------------------------------------------------------------------
/**
* Wrap a SZL DSSE receipt record in a W3C Verifiable Credential v2.
*
* @param receipt - Parsed SZL receipt record from JSONL chain
* @param opts - Wrapping options
* @returns - W3C VC 2.0 document (JSON-LD)
*/
export function wrapDSSEinVC(
receipt: SZLReceiptRecord,
opts: WrapDSSEOptions = {}
): SZLVerifiableCredential {
const issuerDID = opts.issuerDID ?? DEFAULT_ISSUER_DID;
const verificationMethod = opts.verificationMethod ?? DEFAULT_VERIFICATION_METHOD;
// Compute payload hash for credentialSubject
const payloadBytes = decodeBase64url(receipt.envelope.payload);
const payloadHash = sha256Hex(payloadBytes);
// Build credentialSubject
const credentialSubject: SZLReceiptClaims = {
id: receipt.operatorDID ?? `urn:szl:organ:${receipt.organId}`,
organId: receipt.organId,
action: receipt.action,
outcome: receipt.outcome,
operatorDID: receipt.operatorDID,
policyRef: receipt.policyRef,
payloadHash,
dssePayloadType: receipt.envelope.payloadType,
dsseSignatures: receipt.envelope.signatures,
...(receipt.rekorAttestation && { rekorAttestation: receipt.rekorAttestation }),
...(receipt.ipfsCID && { ipfsCID: receipt.ipfsCID }),
...(receipt.webAuthnCredentialId && {
webAuthnCredentialId: receipt.webAuthnCredentialId,
}),
};
// Build unsigned VC
const unsignedVC: Omit<SZLVerifiableCredential, "proof"> = {
"@context": [
"https://www.w3.org/ns/credentials/v2",
SZL_CONTEXT_URL,
],
type: [
"VerifiableCredential",
"SZLGovernanceReceipt",
...(opts.additionalTypes ?? []),
],
id: makeCredentialId(receipt.receiptId),
issuer: {
id: issuerDID,
name: "SZL Holdings",
},
validFrom: receipt.timestamp,
...(opts.validUntil && { validUntil: opts.validUntil }),
credentialSubject,
};
// Build proof
let proof: DataIntegrityProof;
if (opts.signingKeyPem) {
// ecdsa-rdfc-2022 path (dev/CI)
const canonBytes = canonicalizeVC(unsignedVC);
const proofValue = signVCBytes(canonBytes, opts.signingKeyPem);
proof = {
type: "DataIntegrityProof",
cryptosuite: "ecdsa-rdfc-2022",
created: new Date().toISOString(),
verificationMethod,
proofPurpose: "assertionMethod",
proofValue,
};
} else {
// DSSESignature2024: embed DSSE signature as-is
// This custom cryptosuite is SZL-defined; the proofValue encodes the
// base64url of the first DSSE signature.
const dsseProofValue = receipt.envelope.signatures[0]?.sig ?? "";
proof = {
type: "DataIntegrityProof",
cryptosuite: "DSSESignature2024",
created: new Date().toISOString(),
verificationMethod,
proofPurpose: "assertionMethod",
proofValue: dsseProofValue,
};
}
return { ...unsignedVC, proof } as SZLVerifiableCredential;
}
// ---------------------------------------------------------------------------
// Verifiable Presentation builder
// ---------------------------------------------------------------------------
/**
* Wrap multiple SZL VCs in a W3C Verifiable Presentation (VP) for bulk
* presentation to an auditor or SCITT client.
*
* Ref: https://www.w3.org/TR/vc-data-model-2.0/#verifiable-presentations
*/
export interface SZLVerifiablePresentation {
"@context": string[];
type: ["VerifiablePresentation", "SZLGovernanceAuditPresentation"];
id: string;
holder?: string;
verifiableCredential: SZLVerifiableCredential[];
proof?: DataIntegrityProof;
}
export function buildVerifiablePresentation(
vcs: SZLVerifiableCredential[],
holderDID?: string,
presentationId?: string
): SZLVerifiablePresentation {
return {
"@context": [
"https://www.w3.org/ns/credentials/v2",
SZL_CONTEXT_URL,
],
type: ["VerifiablePresentation", "SZLGovernanceAuditPresentation"],
id:
presentationId ??
`urn:szl:presentation:${Date.now()}`,
...(holderDID && { holder: holderDID }),
verifiableCredential: vcs,
};
}
// ---------------------------------------------------------------------------
// JSONL → VC batch converter
// ---------------------------------------------------------------------------
/**
* Convert an array of SZL JSONL receipt records to W3C VCs.
* Suitable for wrapping an entire JSONL chain for audit export.
*/
export function batchWrapReceipts(
receipts: SZLReceiptRecord[],
opts: WrapDSSEOptions = {}
): SZLVerifiableCredential[] {
return receipts.map((r) => wrapDSSEinVC(r, opts));
}
/**
* Convert an array of SZL JSONL receipts to a VP suitable for submission
* to an IETF SCITT transparency service.
*/
export function wrapForSCITT(
receipts: SZLReceiptRecord[],
holderDID: string,
opts: WrapDSSEOptions = {}
): SZLVerifiablePresentation {
const vcs = batchWrapReceipts(receipts, opts);
return buildVerifiablePresentation(
vcs,
holderDID,
`urn:szl:scitt:${Date.now()}`
);
}
// ---------------------------------------------------------------------------
// Serialization helpers
// ---------------------------------------------------------------------------
/** Serialize a VC to compact JSON-LD (no whitespace) — suitable for IPFS pinning */
export function serializeVC(vc: SZLVerifiableCredential): string {
return JSON.stringify(vc);
}
/** Serialize a VC to pretty JSON-LD — suitable for human review */
export function prettySerializeVC(vc: SZLVerifiableCredential): string {
return JSON.stringify(vc, null, 2);
}
// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------
if (import.meta.url === `file://${process.argv[1]}`) {
import("node:fs").then(({ readFileSync, writeFileSync }) => {
const receiptPath = process.argv[2];
if (!receiptPath) {
console.error("Usage: tsx wrap_dsse.ts <receipt.json> [--pretty]");
process.exit(1);
}
const receipt: SZLReceiptRecord = JSON.parse(
readFileSync(receiptPath, "utf8")
);
const { privateKey } = generateKeyPairSync("ec", {
namedCurve: "P-256",
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
});
const vc = wrapDSSEinVC(receipt, { signingKeyPem: privateKey as string });
const out = process.argv.includes("--pretty")
? prettySerializeVC(vc)
: serializeVC(vc);
const outPath = receiptPath.replace(".json", ".vc.json");
writeFileSync(outPath, out);
console.log(`Written to ${outPath}`);
});
}
|