@suveren/gateway 0.19.5 → 0.20.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/content/integrations/crm.json +1 -1
- package/content/integrations/deploy-github.json +1 -1
- package/content/integrations/erp.json +1 -1
- package/content/integrations/mail.json +1 -1
- package/content/integrations/records.json +1 -1
- package/dist/control-plane/index.mjs +21 -13
- package/dist/mcp-server/{chunk-ZRRZFGRL.mjs → chunk-APIQIEUA.mjs} +39 -27
- package/dist/mcp-server/http.mjs +308 -97
- package/dist/mcp-server/report-verify-cli.mjs +1 -1
- package/dist/ui/assets/index-1gCMlbLl.js +116 -0
- package/dist/ui/index.html +1 -1
- package/node_modules/@hap/core/README.md +92 -29
- package/node_modules/@hap/core/dist/index.d.mts +614 -593
- package/node_modules/@hap/core/dist/index.d.ts +614 -593
- package/node_modules/@hap/core/dist/index.js +774 -749
- package/node_modules/@hap/core/dist/index.mjs +744 -724
- package/node_modules/@hap/core/package.json +1 -1
- package/node_modules/@hap/core/src/base64url.ts +39 -0
- package/node_modules/@hap/core/src/commitment-modes.ts +0 -21
- package/node_modules/@hap/core/src/errors.ts +76 -0
- package/node_modules/@hap/core/src/frame.ts +47 -178
- package/node_modules/@hap/core/src/gatekeeper.ts +219 -600
- package/node_modules/@hap/core/src/identity.ts +9 -12
- package/node_modules/@hap/core/src/index.ts +8 -4
- package/node_modules/@hap/core/src/mandate.ts +177 -166
- package/node_modules/@hap/core/src/owner-signature.ts +282 -0
- package/node_modules/@hap/core/src/profile.ts +234 -0
- package/node_modules/@hap/core/src/ticket.ts +148 -0
- package/node_modules/@hap/core/src/types.ts +191 -344
- package/node_modules/@hap/core/src/versions.ts +47 -0
- package/package.json +2 -2
- package/profiles/calendar/0.6.profile.json +191 -0
- package/profiles/charge/0.6.profile.json +211 -0
- package/profiles/customers/0.9.profile.json +182 -0
- package/profiles/delegation/0.3.profile.json +118 -0
- package/profiles/deploy/0.11.profile.json +177 -0
- package/profiles/email/0.8.profile.json +226 -0
- package/profiles/index.json +12 -1
- package/profiles/publish/0.6.profile.json +187 -0
- package/profiles/purchase/0.7.profile.json +235 -0
- package/profiles/records/0.6.profile.json +118 -0
- package/profiles/reporting/0.3.profile.json +98 -0
- package/profiles/sales/0.4.profile.json +291 -0
- package/dist/ui/assets/index-Du68zJhH.js +0 -116
- package/node_modules/@hap/core/src/attestation.ts +0 -173
- package/node_modules/@hap/core/src/receipt.ts +0 -74
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - {@link deriveIdentityLine} — the human-readable footer line for a subject.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import type {
|
|
11
|
+
import type { MandateOwnerEntry, Subject } from './types';
|
|
12
12
|
|
|
13
13
|
export interface SubjectValidation {
|
|
14
14
|
valid: boolean;
|
|
@@ -20,21 +20,18 @@ export interface SubjectValidation {
|
|
|
20
20
|
* - `self_declared` ⇒ `low` / `self`, no disclosed name.
|
|
21
21
|
* - `as_vouched` ⇒ `high` / `as`, `verifier` required.
|
|
22
22
|
* - `eudi` ⇒ `high` / `external`, `verifier` required — and, when the
|
|
23
|
-
* carrying
|
|
23
|
+
* carrying mandate's `mandate_owners` are supplied, a corresponding
|
|
24
24
|
* entry with `binding:"eudi"` for this subject's DID.
|
|
25
25
|
* - `disclose.name` only at `assurance:"high"`.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
* the identity claim, not the mandate (see the field's deprecation note). The
|
|
29
|
-
* signature-bearing object is the attestation's `owner_mandates` entry, which
|
|
27
|
+
* The signature-bearing object is the mandate's `mandate_owners` entry, which
|
|
30
28
|
* is payload-level context this per-subject check cannot see on its own; pass
|
|
31
|
-
* `opts.
|
|
32
|
-
* validate subject shape alone
|
|
33
|
-
* the retired owner_signature requirement).
|
|
29
|
+
* `opts.mandateOwners` to enforce the eudi ⇒ mandate-owner-entry rule, omit it
|
|
30
|
+
* to validate subject shape alone.
|
|
34
31
|
*/
|
|
35
32
|
export function validateSubject(
|
|
36
33
|
subject: Subject,
|
|
37
|
-
opts?: {
|
|
34
|
+
opts?: { mandateOwners?: MandateOwnerEntry[] },
|
|
38
35
|
): SubjectValidation {
|
|
39
36
|
const errors: string[] = [];
|
|
40
37
|
const { did, assurance, method, trust_root, verifier, disclose } = subject;
|
|
@@ -52,9 +49,9 @@ export function validateSubject(
|
|
|
52
49
|
} else if (method === 'eudi') {
|
|
53
50
|
if (trust_root !== 'external') errors.push('eudi requires trust_root "external"');
|
|
54
51
|
if (!verifier) errors.push('eudi requires a verifier');
|
|
55
|
-
if (opts?.
|
|
56
|
-
const entry = opts.
|
|
57
|
-
if (!entry) errors.push('eudi requires
|
|
52
|
+
if (opts?.mandateOwners !== undefined) {
|
|
53
|
+
const entry = opts.mandateOwners.find(m => m.did === did && m.binding === 'eudi');
|
|
54
|
+
if (!entry) errors.push('eudi requires a mandate_owners entry with binding "eudi" for this DID');
|
|
58
55
|
}
|
|
59
56
|
} else {
|
|
60
57
|
errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HAP Core —
|
|
2
|
+
* HAP Core — v0.7 ("mandate" / "ticket" / "scope" / "Mandate Owner")
|
|
3
3
|
*
|
|
4
4
|
* Shared protocol logic for agent-oriented bounded execution.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export * from './types';
|
|
8
8
|
export * from './canonicalize';
|
|
9
|
+
export * from './base64url';
|
|
10
|
+
export * from './errors';
|
|
11
|
+
export * from './versions';
|
|
9
12
|
export * from './content-binding';
|
|
10
13
|
export * from './intent-disclosure';
|
|
11
14
|
export * from './identity';
|
|
12
15
|
export * from './frame';
|
|
13
16
|
export * from './commitment-modes';
|
|
14
|
-
export * from './
|
|
17
|
+
export * from './profile';
|
|
18
|
+
export * from './mandate';
|
|
19
|
+
export * from './owner-signature';
|
|
20
|
+
export * from './ticket';
|
|
15
21
|
export * from './gatekeeper';
|
|
16
22
|
export * from './profiles';
|
|
17
23
|
export * from './did-key';
|
|
18
|
-
export * from './mandate';
|
|
19
|
-
export * from './receipt';
|
|
@@ -1,207 +1,218 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* `HAP-approval` object, and their verification.
|
|
2
|
+
* Mandate encoding, decoding, signing, and verification (v0.7).
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* attestation
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* second fetch, no key directory. See protocol.md → "Owner Mandate Signatures".
|
|
12
|
-
*
|
|
13
|
-
* Verification here requires NO trust in the AS. What it cannot do is tell the
|
|
14
|
-
* verifier WHOSE key signed — confirming the DID belongs to the expected
|
|
15
|
-
* person is the out-of-band step (protocol.md → "Identity DIDs vs signing
|
|
16
|
-
* DIDs"), and it is the honest cost of cold verification.
|
|
4
|
+
* Renamed from `attestation.ts` (protocol.md → *Migration from v0.6*:
|
|
5
|
+
* `header.typ: "HAP-attestation"` → `"HAP-mandate"`, `attestation_id` →
|
|
6
|
+
* `mandate_id`). v0.3's frame-hash path and v0.4's dual frame/bounds
|
|
7
|
+
* verification are retired with the rest of pre-v0.5 — this module issues
|
|
8
|
+
* and verifies `version: "0.7"` mandates only (CLAUDE.md "Decided by the
|
|
9
|
+
* owner": no backward-compat verify path for 0.5/0.6).
|
|
17
10
|
*/
|
|
18
11
|
|
|
12
|
+
import { createHash } from 'crypto';
|
|
19
13
|
import * as ed from '@noble/ed25519';
|
|
20
|
-
import type { Attestation, AttestationPayload, OwnerMandate } from './types';
|
|
21
14
|
import { canonicalize } from './canonicalize';
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/** Projection/canonicalization version. A verifier MUST pin it. */
|
|
28
|
-
version: '0.6';
|
|
29
|
-
profile_id: string;
|
|
30
|
-
/** The signing owner's own DID. MUST be a member of `resolved_owners`. */
|
|
31
|
-
owner_did: string;
|
|
32
|
-
bounds_hash: string;
|
|
33
|
-
context_hash: string;
|
|
34
|
-
execution_context_hash: string;
|
|
35
|
-
gate_content_hashes: Record<string, string>;
|
|
36
|
-
/** Included iff the attestation carries one — binds ciphertext AND the
|
|
37
|
-
* frozen approver set, the swap this mechanism exists to stop. */
|
|
38
|
-
intent_disclosure_hash?: string;
|
|
39
|
-
commitment_mode: string;
|
|
40
|
-
/** The replay defence: the human signs how long the authority lives. */
|
|
41
|
-
expires_at: number;
|
|
42
|
-
nonce: string;
|
|
43
|
-
}
|
|
15
|
+
import { toBase64Url, fromBase64Url } from './base64url';
|
|
16
|
+
import { decodeDidKey, isKeyBearingDid } from './did-key';
|
|
17
|
+
import { HapError } from './errors';
|
|
18
|
+
import { PROTOCOL_VERSION } from './versions';
|
|
19
|
+
import type { Mandate, MandateHeader, MandatePayload } from './types';
|
|
44
20
|
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
typ: 'HAP-approval';
|
|
50
|
-
version: '0.6';
|
|
51
|
-
proposal_id: string;
|
|
52
|
-
attestation_id: string;
|
|
53
|
-
decision: 'commit' | 'reject';
|
|
54
|
-
/** What was approved: the receipt's contentHash where the profile binds
|
|
55
|
-
* content; otherwise sha256 over the JCS of the proposal's argument set. */
|
|
56
|
-
content_hash: string;
|
|
57
|
-
decided_at: number;
|
|
58
|
-
nonce: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export class MandateError extends Error {
|
|
62
|
-
constructor(
|
|
63
|
-
public code:
|
|
64
|
-
| 'MANDATE_SIGNATURE_INVALID'
|
|
65
|
-
| 'APPROVAL_SIGNATURE_INVALID'
|
|
66
|
-
| 'NOT_KEY_BEARING'
|
|
67
|
-
| 'OWNER_NOT_RESOLVED'
|
|
68
|
-
| 'MALFORMED_ATTESTATION',
|
|
69
|
-
message: string,
|
|
70
|
-
) {
|
|
71
|
-
super(`${code}: ${message}`);
|
|
72
|
-
this.name = 'MandateError';
|
|
73
|
-
}
|
|
74
|
-
}
|
|
21
|
+
/** Clock-skew tolerance (Mandate rule 9): `issued_at`/`expires_at` checks
|
|
22
|
+
* tolerate up to 300s, and the tolerance never extends life past
|
|
23
|
+
* `expires_at + 300s`. */
|
|
24
|
+
const CLOCK_SKEW_SECONDS = 300;
|
|
75
25
|
|
|
76
26
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
27
|
+
* Decodes a base64url-encoded mandate blob.
|
|
28
|
+
* @throws HapError `MALFORMED_MANDATE` on anything that is not a strict
|
|
29
|
+
* base64url-encoded JSON mandate.
|
|
80
30
|
*/
|
|
81
|
-
export function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
throw new MandateError('MALFORMED_ATTESTATION', 'mandate projection requires bounds_hash, context_hash and commitment_mode');
|
|
31
|
+
export function decodeMandateBlob(blob: string): Mandate {
|
|
32
|
+
let json: string;
|
|
33
|
+
try {
|
|
34
|
+
json = new TextDecoder().decode(fromBase64Url(blob));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new HapError('MALFORMED_MANDATE', `failed to decode mandate blob: ${(err as Error).message}`);
|
|
88
37
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
owner_did: entry.did,
|
|
94
|
-
bounds_hash,
|
|
95
|
-
context_hash,
|
|
96
|
-
execution_context_hash,
|
|
97
|
-
gate_content_hashes,
|
|
98
|
-
commitment_mode,
|
|
99
|
-
expires_at,
|
|
100
|
-
nonce: entry.nonce,
|
|
101
|
-
};
|
|
102
|
-
if (payload.intent_disclosure_hash !== undefined) {
|
|
103
|
-
projection.intent_disclosure_hash = payload.intent_disclosure_hash;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(json);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
throw new HapError('MALFORMED_MANDATE', `mandate blob did not decode to JSON: ${err}`);
|
|
104
42
|
}
|
|
105
|
-
return projection;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The exact bytes an owner signs: RFC 8785 (JCS) canonical UTF-8. */
|
|
109
|
-
export function mandateSigningBytes(projection: MandateProjection): Uint8Array {
|
|
110
|
-
return new TextEncoder().encode(canonicalize(projection));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** The exact bytes an owner signs for a per-action approval. */
|
|
114
|
-
export function approvalSigningBytes(approval: ApprovalObject): Uint8Array {
|
|
115
|
-
return new TextEncoder().encode(canonicalize(approval));
|
|
116
43
|
}
|
|
117
44
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Encodes a mandate as a base64url blob (no padding).
|
|
47
|
+
*/
|
|
48
|
+
export function encodeMandateBlob(mandate: Mandate): string {
|
|
49
|
+
return toBase64Url(new TextEncoder().encode(JSON.stringify(mandate)));
|
|
122
50
|
}
|
|
123
51
|
|
|
124
|
-
|
|
125
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Computes the mandate blob id (hash of the blob) — renamed from
|
|
54
|
+
* `attestationId`.
|
|
55
|
+
*/
|
|
56
|
+
export function mandateBlobId(blob: string): string {
|
|
57
|
+
const hash = createHash('sha256').update(blob, 'utf8').digest('hex');
|
|
58
|
+
return `sha256:${hash}`;
|
|
126
59
|
}
|
|
127
60
|
|
|
128
61
|
/**
|
|
129
|
-
* Sign a mandate
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
62
|
+
* Sign a mandate payload with the Authority Server's Ed25519 private key.
|
|
63
|
+
*
|
|
64
|
+
* @param opts.kid Optional key id for the envelope header. When present it
|
|
65
|
+
* MUST identify the same key as `payload.issuer` (Mandate rule 8) — this
|
|
66
|
+
* function does not enforce that on the signer's side; `verifyMandateSignature`
|
|
67
|
+
* enforces it on the verifier's side, where a mismatch is actually dangerous.
|
|
133
68
|
*/
|
|
134
|
-
export async function
|
|
135
|
-
|
|
69
|
+
export async function signMandate(
|
|
70
|
+
payload: MandatePayload,
|
|
136
71
|
privateKey: Uint8Array,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
/** Sign an approval object with a raw Ed25519 private key (`raw` binding). */
|
|
143
|
-
export async function signApproval(approval: ApprovalObject, privateKey: Uint8Array): Promise<string> {
|
|
144
|
-
const sig = await ed.signAsync(approvalSigningBytes(approval), privateKey);
|
|
145
|
-
return bytesToBase64url(sig);
|
|
72
|
+
opts?: { kid?: string },
|
|
73
|
+
): Promise<Mandate> {
|
|
74
|
+
const header: MandateHeader = { typ: 'HAP-mandate', alg: 'EdDSA', ...(opts?.kid ? { kid: opts.kid } : {}) };
|
|
75
|
+
const signature = toBase64Url(await ed.signAsync(new TextEncoder().encode(canonicalize(payload)), privateKey));
|
|
76
|
+
return { header, payload, signature };
|
|
146
77
|
}
|
|
147
78
|
|
|
148
79
|
/**
|
|
149
|
-
*
|
|
80
|
+
* Verifies a mandate's Ed25519 signature. The verification key is resolved
|
|
81
|
+
* from `payload.issuer` itself — never from a key offered alongside the
|
|
82
|
+
* artifact (protocol.md → *Ticket Verification* step 1, which states the
|
|
83
|
+
* same rule for tickets; mandates follow it identically).
|
|
150
84
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* What this deliberately does NOT verify: that the DID belongs to the person
|
|
157
|
-
* the verifier expects (out-of-band, step 5) and the AS's own signature over
|
|
158
|
-
* the attestation (verifyAttestationSignature, step 1).
|
|
85
|
+
* @param opts.trustedIssuers When supplied, an `issuer` not in this list is
|
|
86
|
+
* rejected before any cryptography runs — "a verifier MUST reject a [...]
|
|
87
|
+
* whose `issuer` it does not trust" (protocol.md → *Ticket Verification*).
|
|
88
|
+
* @throws Error prefixed `INVALID_SIGNATURE:` on any verification failure.
|
|
159
89
|
*/
|
|
160
|
-
export async function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
throw new MandateError('MANDATE_SIGNATURE_INVALID', `alg ${entry.alg} disagrees with the DID's key type (DID is authoritative)`);
|
|
90
|
+
export async function verifyMandateSignature(
|
|
91
|
+
mandate: Mandate,
|
|
92
|
+
opts?: { trustedIssuers?: readonly string[] },
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
const { issuer } = mandate.payload;
|
|
95
|
+
|
|
96
|
+
if (opts?.trustedIssuers && !opts.trustedIssuers.includes(issuer)) {
|
|
97
|
+
throw new HapError('INVALID_SIGNATURE', `issuer ${JSON.stringify(issuer)} is not a trusted Authority Server`);
|
|
169
98
|
}
|
|
99
|
+
|
|
170
100
|
let publicKey: Uint8Array;
|
|
171
101
|
try {
|
|
172
|
-
publicKey = decodeDidKey(
|
|
102
|
+
publicKey = decodeDidKey(issuer);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Not every valid `issuer` need be a did:key (it MAY be a did:web that
|
|
105
|
+
// resolves to one) — but this package resolves did:key issuers only;
|
|
106
|
+
// anything else cannot be verified offline here.
|
|
107
|
+
throw new HapError('INVALID_SIGNATURE', `cannot resolve a key from issuer ${JSON.stringify(issuer)}: ${(err as Error).message}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Mandate rule 8: a present kid MUST identify the same key as issuer.
|
|
111
|
+
if (mandate.header.kid !== undefined) {
|
|
112
|
+
const fingerprint = issuer.startsWith('did:key:') ? issuer.slice('did:key:'.length) : undefined;
|
|
113
|
+
if (mandate.header.kid !== fingerprint) {
|
|
114
|
+
throw new HapError('INVALID_SIGNATURE', 'header.kid disagrees with issuer\'s key (Mandate rule 8) — the field a verifier cannot forge wins');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const payloadBytes = new TextEncoder().encode(canonicalize(mandate.payload));
|
|
119
|
+
let sigBytes: Uint8Array;
|
|
120
|
+
try {
|
|
121
|
+
sigBytes = fromBase64Url(mandate.signature);
|
|
173
122
|
} catch (err) {
|
|
174
|
-
throw new
|
|
123
|
+
throw new HapError('INVALID_SIGNATURE', `${(err as Error).message}`);
|
|
175
124
|
}
|
|
176
125
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
throw new MandateError('MANDATE_SIGNATURE_INVALID', `owner mandate signature by ${entry.did} does not verify`);
|
|
126
|
+
const isValid = await ed.verifyAsync(sigBytes, payloadBytes, publicKey).catch(() => false);
|
|
127
|
+
if (!isValid) {
|
|
128
|
+
throw new HapError('INVALID_SIGNATURE', 'mandate signature verification failed');
|
|
181
129
|
}
|
|
182
130
|
}
|
|
183
131
|
|
|
184
|
-
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Checks a mandate payload's time rules (Mandate rule 9): `expires_at` is
|
|
134
|
+
* authoritative, with a {@link CLOCK_SKEW_SECONDS} tolerance that never
|
|
135
|
+
* extends life past `expires_at + 300s`.
|
|
136
|
+
*
|
|
137
|
+
* @throws Error prefixed `TTL_EXPIRED:` when expired.
|
|
138
|
+
*/
|
|
139
|
+
export function checkMandateExpiry(
|
|
140
|
+
payload: MandatePayload,
|
|
141
|
+
now: number = Math.floor(Date.now() / 1000),
|
|
142
|
+
): void {
|
|
143
|
+
if (now > payload.expires_at + CLOCK_SKEW_SECONDS) {
|
|
144
|
+
throw new HapError('TTL_EXPIRED', `mandate expired at ${payload.expires_at}, current time is ${now}`);
|
|
191
145
|
}
|
|
192
|
-
return entries;
|
|
193
146
|
}
|
|
194
147
|
|
|
195
|
-
/**
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
148
|
+
/** The signature fields that are present together or absent together on a
|
|
149
|
+
* `mandate_owners` entry (Mandate rule 7). */
|
|
150
|
+
const OWNER_SIGNATURE_FIELDS = ['alg', 'signature', 'signed_at', 'nonce', 'binding', 'signing_surface'] as const;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validates the structural shape of `mandate_owners` (Mandate rule 7):
|
|
154
|
+
* exactly one entry, `did` required, and the signature fields present
|
|
155
|
+
* together or absent together. A co-signing entry's `did` MUST be key-bearing.
|
|
156
|
+
*
|
|
157
|
+
* @throws HapError `MALFORMED_MANDATE` on a cardinality or completeness
|
|
158
|
+
* violation; `OWNER_SIGNATURE_INVALID` when a co-signing DID is not key-bearing.
|
|
159
|
+
*/
|
|
160
|
+
export function validateMandateOwners(payload: MandatePayload): void {
|
|
161
|
+
const owners = payload.mandate_owners;
|
|
162
|
+
if (!Array.isArray(owners) || owners.length !== 1) {
|
|
163
|
+
throw new HapError(
|
|
164
|
+
'MALFORMED_MANDATE',
|
|
165
|
+
`mandate_owners MUST contain exactly one entry in v0.7, got ${Array.isArray(owners) ? owners.length : typeof owners}`,
|
|
166
|
+
);
|
|
202
167
|
}
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
205
|
-
throw new
|
|
168
|
+
const entry = owners[0];
|
|
169
|
+
if (!entry.did) {
|
|
170
|
+
throw new HapError('MALFORMED_MANDATE', 'mandate_owners[0].did is required');
|
|
206
171
|
}
|
|
172
|
+
|
|
173
|
+
const present = OWNER_SIGNATURE_FIELDS.filter((f) => entry[f] !== undefined);
|
|
174
|
+
if (present.length > 0 && present.length < OWNER_SIGNATURE_FIELDS.length) {
|
|
175
|
+
const missing = OWNER_SIGNATURE_FIELDS.filter((f) => entry[f] === undefined);
|
|
176
|
+
throw new HapError(
|
|
177
|
+
'MALFORMED_MANDATE',
|
|
178
|
+
`mandate_owners[0] carries [${present.join(', ')}] but not [${missing.join(', ')}] — ` +
|
|
179
|
+
'signature fields are present together or absent together (Mandate rule 7)',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
if (present.length === OWNER_SIGNATURE_FIELDS.length && !isKeyBearingDid(entry.did)) {
|
|
183
|
+
throw new HapError('OWNER_SIGNATURE_INVALID', `co-signing DID ${entry.did} is not key-bearing`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Full mandate verification: decode, check `version`, verify the AS
|
|
189
|
+
* signature, check expiry, validate `mandate_owners` shape.
|
|
190
|
+
*
|
|
191
|
+
* Deliberately does NOT verify any `mandate_owners` co-signature — that is
|
|
192
|
+
* `verifyOwnerSignature` in `owner-signature.ts`, a separate trust axis the
|
|
193
|
+
* caller opts into.
|
|
194
|
+
*
|
|
195
|
+
* @throws HapError `VERSION_UNSUPPORTED` for anything other than `"0.7"`.
|
|
196
|
+
* @throws Error (`INVALID_SIGNATURE:` / `TTL_EXPIRED:` / `MALFORMED_MANDATE:`
|
|
197
|
+
* prefixed, or `OWNER_SIGNATURE_INVALID` as a HapError) on any other failure.
|
|
198
|
+
*/
|
|
199
|
+
export async function verifyMandate(
|
|
200
|
+
blob: string,
|
|
201
|
+
opts?: { trustedIssuers?: readonly string[]; now?: number },
|
|
202
|
+
): Promise<MandatePayload> {
|
|
203
|
+
const mandate = decodeMandateBlob(blob);
|
|
204
|
+
|
|
205
|
+
if (mandate.payload.version !== PROTOCOL_VERSION) {
|
|
206
|
+
throw new HapError(
|
|
207
|
+
'VERSION_UNSUPPORTED',
|
|
208
|
+
`mandate version ${JSON.stringify(mandate.payload.version)} is not verifiable by this implementation ` +
|
|
209
|
+
`(verifies ${PROTOCOL_VERSION} only — re-issue the mandate)`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
await verifyMandateSignature(mandate, opts);
|
|
214
|
+
checkMandateExpiry(mandate.payload, opts?.now);
|
|
215
|
+
validateMandateOwners(mandate.payload);
|
|
216
|
+
|
|
217
|
+
return mandate.payload;
|
|
207
218
|
}
|