@calimero-network/mero-js 19.20.0 → 19.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -34,8 +34,12 @@ __export(index_exports, {
34
34
  RelayClient: () => RelayClient,
35
35
  RpcClient: () => RpcClient,
36
36
  RpcError: () => RpcError,
37
+ SEALED_CONTENT_TYPE: () => SEALED_CONTENT_TYPE,
38
+ SEALED_PATH: () => SEALED_PATH,
37
39
  SIGNED_NAMESPACE_OP_SCHEMA_VERSION: () => SIGNED_NAMESPACE_OP_SCHEMA_VERSION,
40
+ SealedTransportError: () => SealedTransportError,
38
41
  SseClient: () => SseClient,
42
+ StaleTransportKeyError: () => StaleTransportKeyError,
39
43
  WebHttpClient: () => WebHttpClient,
40
44
  WsClient: () => WsClient,
41
45
  accountForRoot: () => accountForRoot,
@@ -63,11 +67,13 @@ __export(index_exports, {
63
67
  createNodeHttpClient: () => createNodeHttpClient,
64
68
  createProofSigner: () => createProofSigner,
65
69
  createRetryableMethod: () => createRetryableMethod,
70
+ createSealedFetch: () => createSealedFetch,
66
71
  createTimeoutSignal: () => createTimeoutSignal,
67
72
  createUniversalHttpClient: () => createUniversalHttpClient,
68
73
  deviceCertPayload: () => deviceCertPayload,
69
74
  discoverLocalNodes: () => discoverLocalNodes,
70
75
  encodeSignedInvitation: () => encodeSignedInvitation,
76
+ fetchAttestedTransportKey: () => fetchAttestedTransportKey,
71
77
  generateAccountRoot: () => generateAccountRoot,
72
78
  generateSessionKey: () => generateSessionKey,
73
79
  hasCap: () => hasCap,
@@ -90,6 +96,7 @@ __export(index_exports, {
90
96
  signRequest: () => signRequest,
91
97
  signRoutingChallenge: () => signRoutingChallenge,
92
98
  signWarrant: () => signWarrant,
99
+ transportKeyBinding: () => transportKeyBinding,
93
100
  withCap: () => withCap,
94
101
  withRetry: () => withRetry,
95
102
  withoutCap: () => withoutCap
@@ -6587,6 +6594,216 @@ async function defaultNonceSource(deviceSecret) {
6587
6594
  }
6588
6595
  }
6589
6596
 
6597
+ // src/sealed/sealed.ts
6598
+ var SEALED_PATH = "/sealed/v1";
6599
+ var SEALED_CONTENT_TYPE = "application/vnd.calimero.sealed";
6600
+ var VERSION = 1;
6601
+ var NONCE_LEN = 12;
6602
+ var REQUEST_DOMAIN = "calimero/sealed-http/v1/request";
6603
+ var RESPONSE_DOMAIN = "calimero/sealed-http/v1/response";
6604
+ var TRANSPORT_BINDING_DOMAIN = "calimero.tee-attest.transport-key.v1";
6605
+ var PKCS8_X25519_PREFIX = new Uint8Array([
6606
+ 48,
6607
+ 46,
6608
+ 2,
6609
+ 1,
6610
+ 0,
6611
+ 48,
6612
+ 5,
6613
+ 6,
6614
+ 3,
6615
+ 43,
6616
+ 101,
6617
+ 110,
6618
+ 4,
6619
+ 34,
6620
+ 4,
6621
+ 32
6622
+ ]);
6623
+ var utf8 = new TextEncoder();
6624
+ var StaleTransportKeyError = class extends Error {
6625
+ constructor() {
6626
+ super(
6627
+ "The node no longer holds the transport key this request was sealed to; attest again for the current one"
6628
+ );
6629
+ this.name = "StaleTransportKeyError";
6630
+ }
6631
+ };
6632
+ var SealedTransportError = class extends Error {
6633
+ constructor(status, code, message) {
6634
+ super(message);
6635
+ this.status = status;
6636
+ this.code = code;
6637
+ this.name = "SealedTransportError";
6638
+ }
6639
+ };
6640
+ async function transportKeyBinding(inner, transportKey) {
6641
+ const digest = await crypto.subtle.digest(
6642
+ "SHA-256",
6643
+ concat(utf8.encode(TRANSPORT_BINDING_DOMAIN), inner, transportKey)
6644
+ );
6645
+ return new Uint8Array(digest);
6646
+ }
6647
+ async function fetchAttestedTransportKey(admin, verify, options = {}) {
6648
+ if (options.applicationId === void 0 !== (options.applicationHash === void 0)) {
6649
+ throw new Error("applicationId and applicationHash are given together or not at all");
6650
+ }
6651
+ const nonce = hex(crypto.getRandomValues(new Uint8Array(32)));
6652
+ const attested = await admin.teeAttest({
6653
+ nonce,
6654
+ applicationId: options.applicationId,
6655
+ bindTransportKey: true
6656
+ });
6657
+ if (!attested.transportPublicKey) {
6658
+ throw new Error("The node did not report a transport key; it predates sealed transport");
6659
+ }
6660
+ const transportKey = fromHex(attested.transportPublicKey, "transportPublicKey", 32);
6661
+ const inner = options.applicationHash ? fromHex(options.applicationHash, "applicationHash", 32) : new Uint8Array(32);
6662
+ const reportDataSuffix = hex(await transportKeyBinding(inner, transportKey));
6663
+ if (!await verify({ quoteB64: attested.quoteB64, nonce, reportDataSuffix })) {
6664
+ throw new Error("The attestation did not verify, so its transport key is not trusted");
6665
+ }
6666
+ return transportKey;
6667
+ }
6668
+ function createSealedFetch(options) {
6669
+ const base = new URL(options.baseUrl);
6670
+ const basePath = base.pathname.replace(/\/+$/, "");
6671
+ const sealedUrl = `${base.origin}${basePath}${SEALED_PATH}`;
6672
+ const transportKey = options.transportPublicKey;
6673
+ if (transportKey.length !== 32) {
6674
+ throw new Error("transportPublicKey must be 32 bytes");
6675
+ }
6676
+ const baseFetch = options.fetch ?? ((input, init) => fetch(input, init));
6677
+ return async (input, init) => {
6678
+ const request = new Request(input, init);
6679
+ const url = new URL(request.url);
6680
+ if (url.origin !== base.origin || !url.pathname.startsWith(`${basePath}/`)) {
6681
+ throw new Error(`Refusing to send ${url.href} outside the sealed node at ${options.baseUrl}`);
6682
+ }
6683
+ const headers = [];
6684
+ request.headers.forEach((value, name) => headers.push([name, value]));
6685
+ const head = {
6686
+ method: request.method,
6687
+ path: url.pathname.slice(basePath.length) + url.search,
6688
+ headers,
6689
+ ts: Math.floor(Date.now() / 1e3)
6690
+ };
6691
+ const body = new Uint8Array(await request.arrayBuffer());
6692
+ const exchange = await sealRequest(transportKey, head, body);
6693
+ const response = await baseFetch(sealedUrl, {
6694
+ method: "POST",
6695
+ headers: { "content-type": SEALED_CONTENT_TYPE },
6696
+ body: exchange.envelope,
6697
+ signal: request.signal
6698
+ });
6699
+ if (response.headers.get("content-type") !== SEALED_CONTENT_TYPE) {
6700
+ throw await envelopeRefusal(response);
6701
+ }
6702
+ const opened = await exchange.open(new Uint8Array(await response.arrayBuffer()));
6703
+ const nullBody = [101, 204, 205, 304].includes(opened.head.status);
6704
+ return new Response(nullBody ? null : opened.body, {
6705
+ status: opened.head.status,
6706
+ headers: opened.head.headers
6707
+ });
6708
+ };
6709
+ }
6710
+ async function envelopeRefusal(response) {
6711
+ let code;
6712
+ let message = `Sealed request refused: HTTP ${response.status}`;
6713
+ try {
6714
+ const body = await response.json();
6715
+ code = body?.error?.code;
6716
+ if (body?.error?.message) message = `Sealed request refused: ${body.error.message}`;
6717
+ } catch {
6718
+ }
6719
+ if (code === "stale_transport_key") return new StaleTransportKeyError();
6720
+ return new SealedTransportError(response.status, code, message);
6721
+ }
6722
+ async function sealRequest(transportKey, head, body, fixed) {
6723
+ const { privateKey, publicKey } = await clientKeyPair(fixed?.clientSecret);
6724
+ const transport = await crypto.subtle.importKey("raw", transportKey, { name: "X25519" }, false, []);
6725
+ const shared = new Uint8Array(
6726
+ await crypto.subtle.deriveBits({ name: "X25519", public: transport }, privateKey, 256)
6727
+ );
6728
+ if (shared.every((byte) => byte === 0)) {
6729
+ throw new Error("The transport key is a low-order point");
6730
+ }
6731
+ const salt = concat(transportKey, publicKey);
6732
+ const requestKey = await exchangeKey(shared, salt, REQUEST_DOMAIN, "encrypt");
6733
+ const responseKey = await exchangeKey(shared, salt, RESPONSE_DOMAIN, "decrypt");
6734
+ const nonce = fixed?.nonce ?? crypto.getRandomValues(new Uint8Array(NONCE_LEN));
6735
+ const header = concat(new Uint8Array([VERSION]), transportKey, publicKey, nonce);
6736
+ const ciphertext = await crypto.subtle.encrypt(
6737
+ { name: "AES-GCM", iv: nonce, additionalData: header },
6738
+ requestKey,
6739
+ joinInner(head, body)
6740
+ );
6741
+ const responseAad = concat(new Uint8Array([VERSION]), salt);
6742
+ return {
6743
+ envelope: concat(header, new Uint8Array(ciphertext)),
6744
+ async open(sealed) {
6745
+ if (sealed.length < 1 + NONCE_LEN + 16 || sealed[0] !== VERSION) {
6746
+ throw new Error("The sealed response is malformed");
6747
+ }
6748
+ let plaintext;
6749
+ try {
6750
+ plaintext = new Uint8Array(
6751
+ await crypto.subtle.decrypt(
6752
+ { name: "AES-GCM", iv: sealed.slice(1, 1 + NONCE_LEN), additionalData: responseAad },
6753
+ responseKey,
6754
+ sealed.slice(1 + NONCE_LEN)
6755
+ )
6756
+ );
6757
+ } catch {
6758
+ throw new Error("The sealed response did not open: it was not sealed by the attested node");
6759
+ }
6760
+ const view = new DataView(plaintext.buffer, plaintext.byteOffset, plaintext.byteLength);
6761
+ const len = view.getUint32(0, false);
6762
+ const head2 = JSON.parse(new TextDecoder().decode(plaintext.slice(4, 4 + len)));
6763
+ return { head: head2, body: plaintext.slice(4 + len) };
6764
+ }
6765
+ };
6766
+ }
6767
+ function joinInner(head, body) {
6768
+ const encoded = utf8.encode(JSON.stringify(head));
6769
+ const len = new Uint8Array(4);
6770
+ new DataView(len.buffer).setUint32(0, encoded.length, false);
6771
+ return concat(len, encoded, body);
6772
+ }
6773
+ async function exchangeKey(shared, salt, domain, usage) {
6774
+ const ikm = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
6775
+ return crypto.subtle.deriveKey(
6776
+ { name: "HKDF", hash: "SHA-256", salt, info: utf8.encode(domain) },
6777
+ ikm,
6778
+ { name: "AES-GCM", length: 256 },
6779
+ false,
6780
+ [usage]
6781
+ );
6782
+ }
6783
+ async function clientKeyPair(secret) {
6784
+ if (!secret) {
6785
+ const pair = await crypto.subtle.generateKey({ name: "X25519" }, true, [
6786
+ "deriveBits"
6787
+ ]);
6788
+ const publicKey = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
6789
+ return { privateKey: pair.privateKey, publicKey };
6790
+ }
6791
+ const privateKey = await crypto.subtle.importKey(
6792
+ "pkcs8",
6793
+ concat(PKCS8_X25519_PREFIX, secret),
6794
+ { name: "X25519" },
6795
+ true,
6796
+ ["deriveBits"]
6797
+ );
6798
+ const jwk = await crypto.subtle.exportKey("jwk", privateKey);
6799
+ return { privateKey, publicKey: base64UrlDecode(jwk.x) };
6800
+ }
6801
+ function base64UrlDecode(value) {
6802
+ const base643 = value.replace(/-/g, "+").replace(/_/g, "/");
6803
+ const binary = atob(base643 + "=".repeat((4 - base643.length % 4) % 4));
6804
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
6805
+ }
6806
+
6590
6807
  // src/namespace-op/namespace-op.ts
6591
6808
  var SIGNED_NAMESPACE_OP_SCHEMA_VERSION = 8;
6592
6809
  var NAMESPACE_SIGN_DOMAIN = new TextEncoder().encode("calimero.namespace.v1");