@humanagencyp/hap-core 0.5.1 → 0.5.2
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.d.mts +70 -3
- package/dist/index.d.ts +70 -3
- package/dist/index.js +33 -10
- package/dist/index.mjs +30 -10
- package/package.json +1 -1
- package/src/gatekeeper.ts +4 -4
- package/src/index.ts +1 -0
- package/src/intent-disclosure.ts +69 -0
- package/src/types.ts +23 -2
package/dist/index.d.mts
CHANGED
|
@@ -14,7 +14,7 @@ interface ResolvedDomain {
|
|
|
14
14
|
}
|
|
15
15
|
interface AttestationPayload {
|
|
16
16
|
attestation_id: string;
|
|
17
|
-
version: '0.3' | '0.4';
|
|
17
|
+
version: '0.3' | '0.4' | '0.5';
|
|
18
18
|
profile_id: string;
|
|
19
19
|
/** v0.3 (deprecated) — hash of the authorization frame */
|
|
20
20
|
frame_hash?: string;
|
|
@@ -23,8 +23,29 @@ interface AttestationPayload {
|
|
|
23
23
|
/** v0.4 — hash of the context parameters */
|
|
24
24
|
context_hash?: string;
|
|
25
25
|
execution_context_hash: string;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
|
|
28
|
+
* attestations that carry only `resolved_owners` remain valid. During the
|
|
29
|
+
* transition the AS emits BOTH `resolved_domains` (internal coverage) and
|
|
30
|
+
* `resolved_owners` (the v0.5 signed wire field).
|
|
31
|
+
*/
|
|
32
|
+
resolved_domains?: ResolvedDomain[];
|
|
33
|
+
/**
|
|
34
|
+
* v0.5 — the Decision Owner DIDs this attestation covers (person-centric
|
|
35
|
+
* model; replaces the abstract domain in `resolved_domains`). Carried in the
|
|
36
|
+
* signed payload so verifiers bind the action to the human(s) who authorized it.
|
|
37
|
+
*/
|
|
38
|
+
resolved_owners?: string[];
|
|
27
39
|
gate_content_hashes: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
|
|
42
|
+
* carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
|
|
43
|
+
* binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
|
|
44
|
+
* signed payload (see {@link computeIntentDisclosureHash}), so a compromised
|
|
45
|
+
* AS cannot swap the ciphertext/wrapped keys or alter the approver set
|
|
46
|
+
* without invalidating the attestation signature (companion invariant C2).
|
|
47
|
+
*/
|
|
48
|
+
intent_disclosure_hash?: string;
|
|
28
49
|
/**
|
|
29
50
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
51
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
@@ -518,6 +539,52 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
518
539
|
*/
|
|
519
540
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
520
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
544
|
+
*
|
|
545
|
+
* Two shared, environment-independent hashes that the attester (gateway), the
|
|
546
|
+
* Authority Server, and any approver MUST compute identically:
|
|
547
|
+
*
|
|
548
|
+
* 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
|
|
549
|
+
* is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
|
|
550
|
+
* — the same rule as content-binding's `canonicalizeText`) before hashing,
|
|
551
|
+
* so independent multi-owner approvers reproduce an identical hash from the
|
|
552
|
+
* same logical statement. See protocol.md → "Intent canonicalization".
|
|
553
|
+
*
|
|
554
|
+
* 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
|
|
555
|
+
* binding from companion spec `intent-disclosure@0.1`. It commits the
|
|
556
|
+
* encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
|
|
557
|
+
* into the SIGNED attestation payload, so a compromised AS cannot swap the
|
|
558
|
+
* ciphertext/wrapped keys or widen/shrink the approver set without breaking
|
|
559
|
+
* the attestation signature.
|
|
560
|
+
*
|
|
561
|
+
* Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
|
|
562
|
+
*/
|
|
563
|
+
/**
|
|
564
|
+
* `gate_content_hashes.intent` for an intent statement.
|
|
565
|
+
* Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
|
|
566
|
+
*/
|
|
567
|
+
declare function computeIntentHash(intentText: string): string;
|
|
568
|
+
/**
|
|
569
|
+
* The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
|
|
570
|
+
* exposed so an implementer debugging a mismatch can inspect the serialization
|
|
571
|
+
* both sides must agree on:
|
|
572
|
+
*
|
|
573
|
+
* intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
|
|
574
|
+
*
|
|
575
|
+
* where `intent_ciphertext` is the exact base64url string of the disclosure
|
|
576
|
+
* object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
|
|
577
|
+
* RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
|
|
578
|
+
*/
|
|
579
|
+
declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
|
|
580
|
+
/**
|
|
581
|
+
* `intent_disclosure_hash` — `sha256:<hex>` over
|
|
582
|
+
* {@link intentDisclosureCanonicalBytes}. Computed by the attester after
|
|
583
|
+
* encryption and signed into the attestation; recomputed by the AS (at
|
|
584
|
+
* issuance) and by each approver (on receipt) and compared, fail-closed.
|
|
585
|
+
*/
|
|
586
|
+
declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
|
|
587
|
+
|
|
521
588
|
/**
|
|
522
589
|
* Frame Canonicalization for Agent Profiles
|
|
523
590
|
*
|
|
@@ -715,4 +782,4 @@ declare function listProfiles(): string[];
|
|
|
715
782
|
declare function getAllProfiles(): AgentProfile[];
|
|
716
783
|
declare function clearProfiles(): void;
|
|
717
784
|
|
|
718
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
785
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface ResolvedDomain {
|
|
|
14
14
|
}
|
|
15
15
|
interface AttestationPayload {
|
|
16
16
|
attestation_id: string;
|
|
17
|
-
version: '0.3' | '0.4';
|
|
17
|
+
version: '0.3' | '0.4' | '0.5';
|
|
18
18
|
profile_id: string;
|
|
19
19
|
/** v0.3 (deprecated) — hash of the authorization frame */
|
|
20
20
|
frame_hash?: string;
|
|
@@ -23,8 +23,29 @@ interface AttestationPayload {
|
|
|
23
23
|
/** v0.4 — hash of the context parameters */
|
|
24
24
|
context_hash?: string;
|
|
25
25
|
execution_context_hash: string;
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
|
|
28
|
+
* attestations that carry only `resolved_owners` remain valid. During the
|
|
29
|
+
* transition the AS emits BOTH `resolved_domains` (internal coverage) and
|
|
30
|
+
* `resolved_owners` (the v0.5 signed wire field).
|
|
31
|
+
*/
|
|
32
|
+
resolved_domains?: ResolvedDomain[];
|
|
33
|
+
/**
|
|
34
|
+
* v0.5 — the Decision Owner DIDs this attestation covers (person-centric
|
|
35
|
+
* model; replaces the abstract domain in `resolved_domains`). Carried in the
|
|
36
|
+
* signed payload so verifiers bind the action to the human(s) who authorized it.
|
|
37
|
+
*/
|
|
38
|
+
resolved_owners?: string[];
|
|
27
39
|
gate_content_hashes: Record<string, string>;
|
|
40
|
+
/**
|
|
41
|
+
* v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
|
|
42
|
+
* carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
|
|
43
|
+
* binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
|
|
44
|
+
* signed payload (see {@link computeIntentDisclosureHash}), so a compromised
|
|
45
|
+
* AS cannot swap the ciphertext/wrapped keys or alter the approver set
|
|
46
|
+
* without invalidating the attestation signature (companion invariant C2).
|
|
47
|
+
*/
|
|
48
|
+
intent_disclosure_hash?: string;
|
|
28
49
|
/**
|
|
29
50
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
51
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
@@ -518,6 +539,52 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
518
539
|
*/
|
|
519
540
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
520
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
544
|
+
*
|
|
545
|
+
* Two shared, environment-independent hashes that the attester (gateway), the
|
|
546
|
+
* Authority Server, and any approver MUST compute identically:
|
|
547
|
+
*
|
|
548
|
+
* 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
|
|
549
|
+
* is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
|
|
550
|
+
* — the same rule as content-binding's `canonicalizeText`) before hashing,
|
|
551
|
+
* so independent multi-owner approvers reproduce an identical hash from the
|
|
552
|
+
* same logical statement. See protocol.md → "Intent canonicalization".
|
|
553
|
+
*
|
|
554
|
+
* 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
|
|
555
|
+
* binding from companion spec `intent-disclosure@0.1`. It commits the
|
|
556
|
+
* encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
|
|
557
|
+
* into the SIGNED attestation payload, so a compromised AS cannot swap the
|
|
558
|
+
* ciphertext/wrapped keys or widen/shrink the approver set without breaking
|
|
559
|
+
* the attestation signature.
|
|
560
|
+
*
|
|
561
|
+
* Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
|
|
562
|
+
*/
|
|
563
|
+
/**
|
|
564
|
+
* `gate_content_hashes.intent` for an intent statement.
|
|
565
|
+
* Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
|
|
566
|
+
*/
|
|
567
|
+
declare function computeIntentHash(intentText: string): string;
|
|
568
|
+
/**
|
|
569
|
+
* The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
|
|
570
|
+
* exposed so an implementer debugging a mismatch can inspect the serialization
|
|
571
|
+
* both sides must agree on:
|
|
572
|
+
*
|
|
573
|
+
* intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
|
|
574
|
+
*
|
|
575
|
+
* where `intent_ciphertext` is the exact base64url string of the disclosure
|
|
576
|
+
* object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
|
|
577
|
+
* RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
|
|
578
|
+
*/
|
|
579
|
+
declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
|
|
580
|
+
/**
|
|
581
|
+
* `intent_disclosure_hash` — `sha256:<hex>` over
|
|
582
|
+
* {@link intentDisclosureCanonicalBytes}. Computed by the attester after
|
|
583
|
+
* encryption and signed into the attestation; recomputed by the AS (at
|
|
584
|
+
* issuance) and by each approver (on receipt) and compared, fail-closed.
|
|
585
|
+
*/
|
|
586
|
+
declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
|
|
587
|
+
|
|
521
588
|
/**
|
|
522
589
|
* Frame Canonicalization for Agent Profiles
|
|
523
590
|
*
|
|
@@ -715,4 +782,4 @@ declare function listProfiles(): string[];
|
|
|
715
782
|
declare function getAllProfiles(): AgentProfile[];
|
|
716
783
|
declare function clearProfiles(): void;
|
|
717
784
|
|
|
718
|
-
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
|
785
|
+
export { type AgentBoundsParams, type AgentContextParams, type AgentFrameParams, type AgentProfile, type Attestation, type AttestationHeader, type AttestationPayload, type BoundType, type ContentBinding, type CumulativeFieldDef, type CumulativeWindow, type DeclaredFieldDef, type ExecutionContextFieldDef, type ExecutionLogEntry, type ExecutionLogQuery, type ExecutionMappingTransform, type ExecutionMappingValue, type ExecutionPath, type FieldConstraint, type FieldUnit, type GateQuestion, type GatekeeperError, type GatekeeperRequest, type GatekeeperResult, type ProfileBoundsField, type ProfileContextField, type ProfileFrameField, type ProfileToolGating, type ProfileToolGatingEntry, type ResolvedDomain, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.js
CHANGED
|
@@ -42,12 +42,15 @@ __export(index_exports, {
|
|
|
42
42
|
computeContentHash: () => computeContentHash,
|
|
43
43
|
computeContextHash: () => computeContextHash,
|
|
44
44
|
computeFrameHash: () => computeFrameHash,
|
|
45
|
+
computeIntentDisclosureHash: () => computeIntentDisclosureHash,
|
|
46
|
+
computeIntentHash: () => computeIntentHash,
|
|
45
47
|
contentCanonicalBytes: () => contentCanonicalBytes,
|
|
46
48
|
decodeAttestationBlob: () => decodeAttestationBlob,
|
|
47
49
|
encodeAttestationBlob: () => encodeAttestationBlob,
|
|
48
50
|
frameHash: () => frameHash,
|
|
49
51
|
getAllProfiles: () => getAllProfiles,
|
|
50
52
|
getProfile: () => getProfile,
|
|
53
|
+
intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
|
|
51
54
|
isV4Attestation: () => isV4Attestation,
|
|
52
55
|
listProfiles: () => listProfiles,
|
|
53
56
|
registerProfile: () => registerProfile,
|
|
@@ -116,8 +119,25 @@ function computeContentHash(binding, content) {
|
|
|
116
119
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// src/
|
|
122
|
+
// src/intent-disclosure.ts
|
|
120
123
|
var import_crypto2 = require("crypto");
|
|
124
|
+
function sha256Hex2(bytes) {
|
|
125
|
+
return (0, import_crypto2.createHash)("sha256").update(bytes, "utf8").digest("hex");
|
|
126
|
+
}
|
|
127
|
+
function computeIntentHash(intentText) {
|
|
128
|
+
return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
|
|
129
|
+
}
|
|
130
|
+
function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
|
|
131
|
+
const sorted = [...approversFrozen].sort();
|
|
132
|
+
return `${intentCiphertext}
|
|
133
|
+
${canonicalize(sorted)}`;
|
|
134
|
+
}
|
|
135
|
+
function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
|
|
136
|
+
return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/frame.ts
|
|
140
|
+
var import_crypto3 = require("crypto");
|
|
121
141
|
function validateFrameParams(params, profile) {
|
|
122
142
|
const errors = [];
|
|
123
143
|
if (!profile.frameSchema) {
|
|
@@ -154,7 +174,7 @@ function canonicalFrame(params, profile) {
|
|
|
154
174
|
return lines.join("\n");
|
|
155
175
|
}
|
|
156
176
|
function frameHash(canonicalFrameString) {
|
|
157
|
-
const hash = (0,
|
|
177
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
158
178
|
return `sha256:${hash}`;
|
|
159
179
|
}
|
|
160
180
|
function computeFrameHash(params, profile) {
|
|
@@ -238,17 +258,17 @@ function canonicalContext(params, profile) {
|
|
|
238
258
|
}
|
|
239
259
|
function computeBoundsHash(params, profile) {
|
|
240
260
|
const canonical = canonicalBounds(params, profile);
|
|
241
|
-
const hash = (0,
|
|
261
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
242
262
|
return `sha256:${hash}`;
|
|
243
263
|
}
|
|
244
264
|
function computeContextHash(params, profile) {
|
|
245
265
|
const canonical = canonicalContext(params, profile);
|
|
246
|
-
const hash = (0,
|
|
266
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
247
267
|
return `sha256:${hash}`;
|
|
248
268
|
}
|
|
249
269
|
|
|
250
270
|
// src/attestation.ts
|
|
251
|
-
var
|
|
271
|
+
var import_crypto4 = require("crypto");
|
|
252
272
|
var ed = __toESM(require("@noble/ed25519"));
|
|
253
273
|
function decodeAttestationBlob(blob) {
|
|
254
274
|
try {
|
|
@@ -266,7 +286,7 @@ function encodeAttestationBlob(attestation) {
|
|
|
266
286
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
267
287
|
}
|
|
268
288
|
function attestationId(blob) {
|
|
269
|
-
const hash = (0,
|
|
289
|
+
const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
|
|
270
290
|
return `sha256:${hash}`;
|
|
271
291
|
}
|
|
272
292
|
async function verifyAttestationSignature(attestation, publicKeyHex) {
|
|
@@ -396,11 +416,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
396
416
|
try {
|
|
397
417
|
checkAttestationExpiry(attestation.payload, now);
|
|
398
418
|
} catch {
|
|
399
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
419
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
400
420
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
401
421
|
continue;
|
|
402
422
|
}
|
|
403
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
423
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
404
424
|
coveredDomains.add(rd.domain);
|
|
405
425
|
}
|
|
406
426
|
}
|
|
@@ -475,11 +495,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
475
495
|
try {
|
|
476
496
|
checkAttestationExpiry(attestation.payload, now);
|
|
477
497
|
} catch {
|
|
478
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
498
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
479
499
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
480
500
|
continue;
|
|
481
501
|
}
|
|
482
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
502
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
483
503
|
coveredDomains.add(rd.domain);
|
|
484
504
|
}
|
|
485
505
|
}
|
|
@@ -783,12 +803,15 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
783
803
|
computeContentHash,
|
|
784
804
|
computeContextHash,
|
|
785
805
|
computeFrameHash,
|
|
806
|
+
computeIntentDisclosureHash,
|
|
807
|
+
computeIntentHash,
|
|
786
808
|
contentCanonicalBytes,
|
|
787
809
|
decodeAttestationBlob,
|
|
788
810
|
encodeAttestationBlob,
|
|
789
811
|
frameHash,
|
|
790
812
|
getAllProfiles,
|
|
791
813
|
getProfile,
|
|
814
|
+
intentDisclosureCanonicalBytes,
|
|
792
815
|
isV4Attestation,
|
|
793
816
|
listProfiles,
|
|
794
817
|
registerProfile,
|
package/dist/index.mjs
CHANGED
|
@@ -50,8 +50,25 @@ function computeContentHash(binding, content) {
|
|
|
50
50
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
// src/
|
|
53
|
+
// src/intent-disclosure.ts
|
|
54
54
|
import { createHash as createHash2 } from "crypto";
|
|
55
|
+
function sha256Hex2(bytes) {
|
|
56
|
+
return createHash2("sha256").update(bytes, "utf8").digest("hex");
|
|
57
|
+
}
|
|
58
|
+
function computeIntentHash(intentText) {
|
|
59
|
+
return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
|
|
60
|
+
}
|
|
61
|
+
function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
|
|
62
|
+
const sorted = [...approversFrozen].sort();
|
|
63
|
+
return `${intentCiphertext}
|
|
64
|
+
${canonicalize(sorted)}`;
|
|
65
|
+
}
|
|
66
|
+
function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
|
|
67
|
+
return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/frame.ts
|
|
71
|
+
import { createHash as createHash3 } from "crypto";
|
|
55
72
|
function validateFrameParams(params, profile) {
|
|
56
73
|
const errors = [];
|
|
57
74
|
if (!profile.frameSchema) {
|
|
@@ -88,7 +105,7 @@ function canonicalFrame(params, profile) {
|
|
|
88
105
|
return lines.join("\n");
|
|
89
106
|
}
|
|
90
107
|
function frameHash(canonicalFrameString) {
|
|
91
|
-
const hash =
|
|
108
|
+
const hash = createHash3("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
92
109
|
return `sha256:${hash}`;
|
|
93
110
|
}
|
|
94
111
|
function computeFrameHash(params, profile) {
|
|
@@ -172,17 +189,17 @@ function canonicalContext(params, profile) {
|
|
|
172
189
|
}
|
|
173
190
|
function computeBoundsHash(params, profile) {
|
|
174
191
|
const canonical = canonicalBounds(params, profile);
|
|
175
|
-
const hash =
|
|
192
|
+
const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
|
|
176
193
|
return `sha256:${hash}`;
|
|
177
194
|
}
|
|
178
195
|
function computeContextHash(params, profile) {
|
|
179
196
|
const canonical = canonicalContext(params, profile);
|
|
180
|
-
const hash =
|
|
197
|
+
const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
|
|
181
198
|
return `sha256:${hash}`;
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
// src/attestation.ts
|
|
185
|
-
import { createHash as
|
|
202
|
+
import { createHash as createHash4 } from "crypto";
|
|
186
203
|
import * as ed from "@noble/ed25519";
|
|
187
204
|
function decodeAttestationBlob(blob) {
|
|
188
205
|
try {
|
|
@@ -200,7 +217,7 @@ function encodeAttestationBlob(attestation) {
|
|
|
200
217
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
201
218
|
}
|
|
202
219
|
function attestationId(blob) {
|
|
203
|
-
const hash =
|
|
220
|
+
const hash = createHash4("sha256").update(blob, "utf8").digest("hex");
|
|
204
221
|
return `sha256:${hash}`;
|
|
205
222
|
}
|
|
206
223
|
async function verifyAttestationSignature(attestation, publicKeyHex) {
|
|
@@ -330,11 +347,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
330
347
|
try {
|
|
331
348
|
checkAttestationExpiry(attestation.payload, now);
|
|
332
349
|
} catch {
|
|
333
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
350
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
334
351
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
335
352
|
continue;
|
|
336
353
|
}
|
|
337
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
354
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
338
355
|
coveredDomains.add(rd.domain);
|
|
339
356
|
}
|
|
340
357
|
}
|
|
@@ -409,11 +426,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
409
426
|
try {
|
|
410
427
|
checkAttestationExpiry(attestation.payload, now);
|
|
411
428
|
} catch {
|
|
412
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
429
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
413
430
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
414
431
|
continue;
|
|
415
432
|
}
|
|
416
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
433
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
417
434
|
coveredDomains.add(rd.domain);
|
|
418
435
|
}
|
|
419
436
|
}
|
|
@@ -716,12 +733,15 @@ export {
|
|
|
716
733
|
computeContentHash,
|
|
717
734
|
computeContextHash,
|
|
718
735
|
computeFrameHash,
|
|
736
|
+
computeIntentDisclosureHash,
|
|
737
|
+
computeIntentHash,
|
|
719
738
|
contentCanonicalBytes,
|
|
720
739
|
decodeAttestationBlob,
|
|
721
740
|
encodeAttestationBlob,
|
|
722
741
|
frameHash,
|
|
723
742
|
getAllProfiles,
|
|
724
743
|
getProfile,
|
|
744
|
+
intentDisclosureCanonicalBytes,
|
|
725
745
|
isV4Attestation,
|
|
726
746
|
listProfiles,
|
|
727
747
|
registerProfile,
|
package/package.json
CHANGED
package/src/gatekeeper.ts
CHANGED
|
@@ -131,12 +131,12 @@ async function verifyV3(
|
|
|
131
131
|
try {
|
|
132
132
|
checkAttestationExpiry(attestation.payload, now);
|
|
133
133
|
} catch {
|
|
134
|
-
const domainNames = attestation.payload.resolved_domains.map(d => d.domain).join(', ');
|
|
134
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map(d => d.domain).join(', ');
|
|
135
135
|
errors.push({ code: 'TTL_EXPIRED', message: `Attestation for domain "${domainNames}" has expired` });
|
|
136
136
|
continue;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
139
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
140
140
|
coveredDomains.add(rd.domain);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
@@ -244,12 +244,12 @@ async function verifyV4(
|
|
|
244
244
|
try {
|
|
245
245
|
checkAttestationExpiry(attestation.payload, now);
|
|
246
246
|
} catch {
|
|
247
|
-
const domainNames = attestation.payload.resolved_domains.map(d => d.domain).join(', ');
|
|
247
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map(d => d.domain).join(', ');
|
|
248
248
|
errors.push({ code: 'TTL_EXPIRED', message: `Attestation for domain "${domainNames}" has expired` });
|
|
249
249
|
continue;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
252
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
253
253
|
coveredDomains.add(rd.domain);
|
|
254
254
|
}
|
|
255
255
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
3
|
+
*
|
|
4
|
+
* Two shared, environment-independent hashes that the attester (gateway), the
|
|
5
|
+
* Authority Server, and any approver MUST compute identically:
|
|
6
|
+
*
|
|
7
|
+
* 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
|
|
8
|
+
* is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
|
|
9
|
+
* — the same rule as content-binding's `canonicalizeText`) before hashing,
|
|
10
|
+
* so independent multi-owner approvers reproduce an identical hash from the
|
|
11
|
+
* same logical statement. See protocol.md → "Intent canonicalization".
|
|
12
|
+
*
|
|
13
|
+
* 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
|
|
14
|
+
* binding from companion spec `intent-disclosure@0.1`. It commits the
|
|
15
|
+
* encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
|
|
16
|
+
* into the SIGNED attestation payload, so a compromised AS cannot swap the
|
|
17
|
+
* ciphertext/wrapped keys or widen/shrink the approver set without breaking
|
|
18
|
+
* the attestation signature.
|
|
19
|
+
*
|
|
20
|
+
* Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { createHash } from 'crypto';
|
|
24
|
+
import { canonicalize } from './canonicalize';
|
|
25
|
+
import { canonicalizeText } from './content-binding';
|
|
26
|
+
|
|
27
|
+
function sha256Hex(bytes: string): string {
|
|
28
|
+
return createHash('sha256').update(bytes, 'utf8').digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `gate_content_hashes.intent` for an intent statement.
|
|
33
|
+
* Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
|
|
34
|
+
*/
|
|
35
|
+
export function computeIntentHash(intentText: string): string {
|
|
36
|
+
return `sha256:${sha256Hex(canonicalizeText(intentText))}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
|
|
41
|
+
* exposed so an implementer debugging a mismatch can inspect the serialization
|
|
42
|
+
* both sides must agree on:
|
|
43
|
+
*
|
|
44
|
+
* intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
|
|
45
|
+
*
|
|
46
|
+
* where `intent_ciphertext` is the exact base64url string of the disclosure
|
|
47
|
+
* object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
|
|
48
|
+
* RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
|
|
49
|
+
*/
|
|
50
|
+
export function intentDisclosureCanonicalBytes(
|
|
51
|
+
intentCiphertext: string,
|
|
52
|
+
approversFrozen: string[],
|
|
53
|
+
): string {
|
|
54
|
+
const sorted = [...approversFrozen].sort();
|
|
55
|
+
return `${intentCiphertext}\n${canonicalize(sorted)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* `intent_disclosure_hash` — `sha256:<hex>` over
|
|
60
|
+
* {@link intentDisclosureCanonicalBytes}. Computed by the attester after
|
|
61
|
+
* encryption and signed into the attestation; recomputed by the AS (at
|
|
62
|
+
* issuance) and by each approver (on receipt) and compared, fail-closed.
|
|
63
|
+
*/
|
|
64
|
+
export function computeIntentDisclosureHash(
|
|
65
|
+
intentCiphertext: string,
|
|
66
|
+
approversFrozen: string[],
|
|
67
|
+
): string {
|
|
68
|
+
return `sha256:${sha256Hex(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
|
|
69
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ResolvedDomain {
|
|
|
19
19
|
|
|
20
20
|
export interface AttestationPayload {
|
|
21
21
|
attestation_id: string;
|
|
22
|
-
version: '0.3' | '0.4';
|
|
22
|
+
version: '0.3' | '0.4' | '0.5';
|
|
23
23
|
profile_id: string;
|
|
24
24
|
/** v0.3 (deprecated) — hash of the authorization frame */
|
|
25
25
|
frame_hash?: string;
|
|
@@ -28,8 +28,29 @@ export interface AttestationPayload {
|
|
|
28
28
|
/** v0.4 — hash of the context parameters */
|
|
29
29
|
context_hash?: string;
|
|
30
30
|
execution_context_hash: string;
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* v0.4 (deprecated in v0.5) — domain-scoped owners. Optional so v0.5
|
|
33
|
+
* attestations that carry only `resolved_owners` remain valid. During the
|
|
34
|
+
* transition the AS emits BOTH `resolved_domains` (internal coverage) and
|
|
35
|
+
* `resolved_owners` (the v0.5 signed wire field).
|
|
36
|
+
*/
|
|
37
|
+
resolved_domains?: ResolvedDomain[];
|
|
38
|
+
/**
|
|
39
|
+
* v0.5 — the Decision Owner DIDs this attestation covers (person-centric
|
|
40
|
+
* model; replaces the abstract domain in `resolved_domains`). Carried in the
|
|
41
|
+
* signed payload so verifiers bind the action to the human(s) who authorized it.
|
|
42
|
+
*/
|
|
43
|
+
resolved_owners?: string[];
|
|
32
44
|
gate_content_hashes: Record<string, string>;
|
|
45
|
+
/**
|
|
46
|
+
* v0.5 (companion spec `intent-disclosure@0.1`) — present iff the attestation
|
|
47
|
+
* carries an encrypted-intent disclosure object. `sha256:`-prefixed hash that
|
|
48
|
+
* binds the disclosure's `intent_ciphertext` + `approvers_frozen` into the
|
|
49
|
+
* signed payload (see {@link computeIntentDisclosureHash}), so a compromised
|
|
50
|
+
* AS cannot swap the ciphertext/wrapped keys or alter the approver set
|
|
51
|
+
* without invalidating the attestation signature (companion invariant C2).
|
|
52
|
+
*/
|
|
53
|
+
intent_disclosure_hash?: string;
|
|
33
54
|
/**
|
|
34
55
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
35
56
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|