@humanagencyp/hap-core 0.5.1 → 0.6.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.d.mts +147 -3
- package/dist/index.d.ts +147 -3
- package/dist/index.js +74 -10
- package/dist/index.mjs +69 -10
- package/package.json +1 -1
- package/src/gatekeeper.ts +4 -4
- package/src/identity.ts +77 -0
- package/src/index.ts +2 -0
- package/src/intent-disclosure.ts +69 -0
- package/src/types.ts +62 -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,37 @@ 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;
|
|
49
|
+
/**
|
|
50
|
+
* v0.6 (Identity Assurance) — optional signed overlay carrying the verified
|
|
51
|
+
* real-world identity of the Decision Owner(s), one entry per owner. Present
|
|
52
|
+
* only when identity is disclosed; an attestation with no `subjects` renders
|
|
53
|
+
* as `low` (pseudonymous, no name). See {@link Subject} and
|
|
54
|
+
* {@link deriveIdentityLine}.
|
|
55
|
+
*/
|
|
56
|
+
subjects?: Subject[];
|
|
28
57
|
/**
|
|
29
58
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
59
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
@@ -38,6 +67,38 @@ interface AttestationPayload {
|
|
|
38
67
|
issued_at: number;
|
|
39
68
|
expires_at: number;
|
|
40
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
|
|
72
|
+
* real-world identity to the attestation, gated by HOW the identity was verified.
|
|
73
|
+
*
|
|
74
|
+
* Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
|
|
75
|
+
* name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
|
|
76
|
+
* within its own domain) and `external` (an external eID such as EUDI — carries the
|
|
77
|
+
* owner's own signature, AS-independent). See review.md → "Identity Assurance".
|
|
78
|
+
*/
|
|
79
|
+
interface Subject {
|
|
80
|
+
/** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
|
|
81
|
+
did: string;
|
|
82
|
+
/** `low` → no name shown; `high` → the name MAY be shown. */
|
|
83
|
+
assurance: 'low' | 'high';
|
|
84
|
+
/** How identity was established. */
|
|
85
|
+
method: 'self_declared' | 'as_vouched' | 'eudi';
|
|
86
|
+
/** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
|
|
87
|
+
trust_root: 'self' | 'as' | 'external';
|
|
88
|
+
/** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
|
|
89
|
+
verifier?: string;
|
|
90
|
+
/** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
|
|
91
|
+
disclose?: {
|
|
92
|
+
name: string;
|
|
93
|
+
};
|
|
94
|
+
/** When the underlying verification was performed (unix seconds). */
|
|
95
|
+
verified_at?: number;
|
|
96
|
+
/**
|
|
97
|
+
* eudi only (Phase 2) — the owner's per-event wallet signature, making the
|
|
98
|
+
* identity claim non-repudiable independent of the AS. Null/absent otherwise.
|
|
99
|
+
*/
|
|
100
|
+
owner_signature?: string | null;
|
|
101
|
+
}
|
|
41
102
|
interface Attestation {
|
|
42
103
|
header: AttestationHeader;
|
|
43
104
|
payload: AttestationPayload;
|
|
@@ -518,6 +579,89 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
518
579
|
*/
|
|
519
580
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
520
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
584
|
+
*
|
|
585
|
+
* Two shared, environment-independent hashes that the attester (gateway), the
|
|
586
|
+
* Authority Server, and any approver MUST compute identically:
|
|
587
|
+
*
|
|
588
|
+
* 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
|
|
589
|
+
* is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
|
|
590
|
+
* — the same rule as content-binding's `canonicalizeText`) before hashing,
|
|
591
|
+
* so independent multi-owner approvers reproduce an identical hash from the
|
|
592
|
+
* same logical statement. See protocol.md → "Intent canonicalization".
|
|
593
|
+
*
|
|
594
|
+
* 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
|
|
595
|
+
* binding from companion spec `intent-disclosure@0.1`. It commits the
|
|
596
|
+
* encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
|
|
597
|
+
* into the SIGNED attestation payload, so a compromised AS cannot swap the
|
|
598
|
+
* ciphertext/wrapped keys or widen/shrink the approver set without breaking
|
|
599
|
+
* the attestation signature.
|
|
600
|
+
*
|
|
601
|
+
* Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* `gate_content_hashes.intent` for an intent statement.
|
|
605
|
+
* Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
|
|
606
|
+
*/
|
|
607
|
+
declare function computeIntentHash(intentText: string): string;
|
|
608
|
+
/**
|
|
609
|
+
* The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
|
|
610
|
+
* exposed so an implementer debugging a mismatch can inspect the serialization
|
|
611
|
+
* both sides must agree on:
|
|
612
|
+
*
|
|
613
|
+
* intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
|
|
614
|
+
*
|
|
615
|
+
* where `intent_ciphertext` is the exact base64url string of the disclosure
|
|
616
|
+
* object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
|
|
617
|
+
* RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
|
|
618
|
+
*/
|
|
619
|
+
declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
|
|
620
|
+
/**
|
|
621
|
+
* `intent_disclosure_hash` — `sha256:<hex>` over
|
|
622
|
+
* {@link intentDisclosureCanonicalBytes}. Computed by the attester after
|
|
623
|
+
* encryption and signed into the attestation; recomputed by the AS (at
|
|
624
|
+
* issuance) and by each approver (on receipt) and compared, fail-closed.
|
|
625
|
+
*/
|
|
626
|
+
declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
|
|
630
|
+
* {@link Subject} overlay. See review.md → "Identity Assurance".
|
|
631
|
+
*
|
|
632
|
+
* Two pure helpers, the single source of truth for both the gateway footer and
|
|
633
|
+
* the AS verify page:
|
|
634
|
+
* - {@link validateSubject} — enforces the level/method/trust-root invariants.
|
|
635
|
+
* - {@link deriveIdentityLine} — the human-readable footer line for a subject.
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
interface SubjectValidation {
|
|
639
|
+
valid: boolean;
|
|
640
|
+
errors: string[];
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Enforce the Identity-Assurance invariants on a single subject:
|
|
644
|
+
* - `self_declared` ⇒ `low` / `self`, no disclosed name.
|
|
645
|
+
* - `as_vouched` ⇒ `high` / `as`, `verifier` required.
|
|
646
|
+
* - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
|
|
647
|
+
* - `disclose.name` only at `assurance:"high"`.
|
|
648
|
+
*/
|
|
649
|
+
declare function validateSubject(subject: Subject): SubjectValidation;
|
|
650
|
+
/**
|
|
651
|
+
* The footer line for a subject — the ONE place this wording lives.
|
|
652
|
+
* The name appears only at `high` with a disclosed name; otherwise no name.
|
|
653
|
+
*
|
|
654
|
+
* - low / no disclosure → "Sent by an AI agent via «operator»"
|
|
655
|
+
* - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
|
|
656
|
+
* - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
|
|
657
|
+
*
|
|
658
|
+
* `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
|
|
659
|
+
* upstream; a different operator self-vouches under its own name.
|
|
660
|
+
*/
|
|
661
|
+
declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
|
|
662
|
+
operatorName: string;
|
|
663
|
+
}): string;
|
|
664
|
+
|
|
521
665
|
/**
|
|
522
666
|
* Frame Canonicalization for Agent Profiles
|
|
523
667
|
*
|
|
@@ -715,4 +859,4 @@ declare function listProfiles(): string[];
|
|
|
715
859
|
declare function getAllProfiles(): AgentProfile[];
|
|
716
860
|
declare function clearProfiles(): void;
|
|
717
861
|
|
|
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 };
|
|
862
|
+
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, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, 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,37 @@ 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;
|
|
49
|
+
/**
|
|
50
|
+
* v0.6 (Identity Assurance) — optional signed overlay carrying the verified
|
|
51
|
+
* real-world identity of the Decision Owner(s), one entry per owner. Present
|
|
52
|
+
* only when identity is disclosed; an attestation with no `subjects` renders
|
|
53
|
+
* as `low` (pseudonymous, no name). See {@link Subject} and
|
|
54
|
+
* {@link deriveIdentityLine}.
|
|
55
|
+
*/
|
|
56
|
+
subjects?: Subject[];
|
|
28
57
|
/**
|
|
29
58
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
30
59
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
@@ -38,6 +67,38 @@ interface AttestationPayload {
|
|
|
38
67
|
issued_at: number;
|
|
39
68
|
expires_at: number;
|
|
40
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
|
|
72
|
+
* real-world identity to the attestation, gated by HOW the identity was verified.
|
|
73
|
+
*
|
|
74
|
+
* Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
|
|
75
|
+
* name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
|
|
76
|
+
* within its own domain) and `external` (an external eID such as EUDI — carries the
|
|
77
|
+
* owner's own signature, AS-independent). See review.md → "Identity Assurance".
|
|
78
|
+
*/
|
|
79
|
+
interface Subject {
|
|
80
|
+
/** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
|
|
81
|
+
did: string;
|
|
82
|
+
/** `low` → no name shown; `high` → the name MAY be shown. */
|
|
83
|
+
assurance: 'low' | 'high';
|
|
84
|
+
/** How identity was established. */
|
|
85
|
+
method: 'self_declared' | 'as_vouched' | 'eudi';
|
|
86
|
+
/** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
|
|
87
|
+
trust_root: 'self' | 'as' | 'external';
|
|
88
|
+
/** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
|
|
89
|
+
verifier?: string;
|
|
90
|
+
/** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
|
|
91
|
+
disclose?: {
|
|
92
|
+
name: string;
|
|
93
|
+
};
|
|
94
|
+
/** When the underlying verification was performed (unix seconds). */
|
|
95
|
+
verified_at?: number;
|
|
96
|
+
/**
|
|
97
|
+
* eudi only (Phase 2) — the owner's per-event wallet signature, making the
|
|
98
|
+
* identity claim non-repudiable independent of the AS. Null/absent otherwise.
|
|
99
|
+
*/
|
|
100
|
+
owner_signature?: string | null;
|
|
101
|
+
}
|
|
41
102
|
interface Attestation {
|
|
42
103
|
header: AttestationHeader;
|
|
43
104
|
payload: AttestationPayload;
|
|
@@ -518,6 +579,89 @@ declare function contentCanonicalBytes(kind: ContentBinding['kind'], content: Re
|
|
|
518
579
|
*/
|
|
519
580
|
declare function computeContentHash(binding: ContentBinding, content: Record<string, unknown> | string): string;
|
|
520
581
|
|
|
582
|
+
/**
|
|
583
|
+
* Intent hashing + intent-disclosure binding (HAP v0.5).
|
|
584
|
+
*
|
|
585
|
+
* Two shared, environment-independent hashes that the attester (gateway), the
|
|
586
|
+
* Authority Server, and any approver MUST compute identically:
|
|
587
|
+
*
|
|
588
|
+
* 1. {@link computeIntentHash} — `gate_content_hashes.intent`. The intent text
|
|
589
|
+
* is canonicalized (Unicode NFC, LF line endings, trailing-whitespace strip
|
|
590
|
+
* — the same rule as content-binding's `canonicalizeText`) before hashing,
|
|
591
|
+
* so independent multi-owner approvers reproduce an identical hash from the
|
|
592
|
+
* same logical statement. See protocol.md → "Intent canonicalization".
|
|
593
|
+
*
|
|
594
|
+
* 2. {@link computeIntentDisclosureHash} — `intent_disclosure_hash`, the C2
|
|
595
|
+
* binding from companion spec `intent-disclosure@0.1`. It commits the
|
|
596
|
+
* encrypted disclosure object (`intent_ciphertext` + `approvers_frozen`)
|
|
597
|
+
* into the SIGNED attestation payload, so a compromised AS cannot swap the
|
|
598
|
+
* ciphertext/wrapped keys or widen/shrink the approver set without breaking
|
|
599
|
+
* the attestation signature.
|
|
600
|
+
*
|
|
601
|
+
* Both produce `sha256:<hex>` to match the frame/bounds/context hash format.
|
|
602
|
+
*/
|
|
603
|
+
/**
|
|
604
|
+
* `gate_content_hashes.intent` for an intent statement.
|
|
605
|
+
* Canonicalizes the text (NFC / LF / trailing-whitespace strip) before hashing.
|
|
606
|
+
*/
|
|
607
|
+
declare function computeIntentHash(intentText: string): string;
|
|
608
|
+
/**
|
|
609
|
+
* The exact bytes `intent_disclosure_hash` is taken over, WITHOUT hashing —
|
|
610
|
+
* exposed so an implementer debugging a mismatch can inspect the serialization
|
|
611
|
+
* both sides must agree on:
|
|
612
|
+
*
|
|
613
|
+
* intent_ciphertext ‖ "\n" ‖ JCS(sort(approvers_frozen))
|
|
614
|
+
*
|
|
615
|
+
* where `intent_ciphertext` is the exact base64url string of the disclosure
|
|
616
|
+
* object, `sort` orders the approver DIDs by Unicode code point, and `JCS` is
|
|
617
|
+
* RFC 8785 canonicalization of the sorted array (via {@link canonicalize}).
|
|
618
|
+
*/
|
|
619
|
+
declare function intentDisclosureCanonicalBytes(intentCiphertext: string, approversFrozen: string[]): string;
|
|
620
|
+
/**
|
|
621
|
+
* `intent_disclosure_hash` — `sha256:<hex>` over
|
|
622
|
+
* {@link intentDisclosureCanonicalBytes}. Computed by the attester after
|
|
623
|
+
* encryption and signed into the attestation; recomputed by the AS (at
|
|
624
|
+
* issuance) and by each approver (on receipt) and compared, fail-closed.
|
|
625
|
+
*/
|
|
626
|
+
declare function computeIntentDisclosureHash(intentCiphertext: string, approversFrozen: string[]): string;
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
|
|
630
|
+
* {@link Subject} overlay. See review.md → "Identity Assurance".
|
|
631
|
+
*
|
|
632
|
+
* Two pure helpers, the single source of truth for both the gateway footer and
|
|
633
|
+
* the AS verify page:
|
|
634
|
+
* - {@link validateSubject} — enforces the level/method/trust-root invariants.
|
|
635
|
+
* - {@link deriveIdentityLine} — the human-readable footer line for a subject.
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
interface SubjectValidation {
|
|
639
|
+
valid: boolean;
|
|
640
|
+
errors: string[];
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Enforce the Identity-Assurance invariants on a single subject:
|
|
644
|
+
* - `self_declared` ⇒ `low` / `self`, no disclosed name.
|
|
645
|
+
* - `as_vouched` ⇒ `high` / `as`, `verifier` required.
|
|
646
|
+
* - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
|
|
647
|
+
* - `disclose.name` only at `assurance:"high"`.
|
|
648
|
+
*/
|
|
649
|
+
declare function validateSubject(subject: Subject): SubjectValidation;
|
|
650
|
+
/**
|
|
651
|
+
* The footer line for a subject — the ONE place this wording lives.
|
|
652
|
+
* The name appears only at `high` with a disclosed name; otherwise no name.
|
|
653
|
+
*
|
|
654
|
+
* - low / no disclosure → "Sent by an AI agent via «operator»"
|
|
655
|
+
* - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
|
|
656
|
+
* - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
|
|
657
|
+
*
|
|
658
|
+
* `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
|
|
659
|
+
* upstream; a different operator self-vouches under its own name.
|
|
660
|
+
*/
|
|
661
|
+
declare function deriveIdentityLine(subject: Subject | undefined | null, { operatorName }: {
|
|
662
|
+
operatorName: string;
|
|
663
|
+
}): string;
|
|
664
|
+
|
|
521
665
|
/**
|
|
522
666
|
* Frame Canonicalization for Agent Profiles
|
|
523
667
|
*
|
|
@@ -715,4 +859,4 @@ declare function listProfiles(): string[];
|
|
|
715
859
|
declare function getAllProfiles(): AgentProfile[];
|
|
716
860
|
declare function clearProfiles(): void;
|
|
717
861
|
|
|
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 };
|
|
862
|
+
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, type Subject, type SubjectValidation, attestationId, canonicalBounds, canonicalContext, canonicalFrame, canonicalize, canonicalizeText, checkAttestationExpiry, clearProfiles, computeBoundsHash, computeContentHash, computeContextHash, computeFrameHash, computeIntentDisclosureHash, computeIntentHash, contentCanonicalBytes, decodeAttestationBlob, deriveIdentityLine, encodeAttestationBlob, frameHash, getAllProfiles, getProfile, intentDisclosureCanonicalBytes, isV4Attestation, listProfiles, registerProfile, validateBoundsParams, validateContextParams, validateFrameParams, validateSubject, verify, verifyAttestation, verifyAttestationSignature, verifyAttestationV4, verifyBoundsHash, verifyContextHash, verifyFrameHash };
|
package/dist/index.js
CHANGED
|
@@ -42,18 +42,23 @@ __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,
|
|
49
|
+
deriveIdentityLine: () => deriveIdentityLine,
|
|
47
50
|
encodeAttestationBlob: () => encodeAttestationBlob,
|
|
48
51
|
frameHash: () => frameHash,
|
|
49
52
|
getAllProfiles: () => getAllProfiles,
|
|
50
53
|
getProfile: () => getProfile,
|
|
54
|
+
intentDisclosureCanonicalBytes: () => intentDisclosureCanonicalBytes,
|
|
51
55
|
isV4Attestation: () => isV4Attestation,
|
|
52
56
|
listProfiles: () => listProfiles,
|
|
53
57
|
registerProfile: () => registerProfile,
|
|
54
58
|
validateBoundsParams: () => validateBoundsParams,
|
|
55
59
|
validateContextParams: () => validateContextParams,
|
|
56
60
|
validateFrameParams: () => validateFrameParams,
|
|
61
|
+
validateSubject: () => validateSubject,
|
|
57
62
|
verify: () => verify,
|
|
58
63
|
verifyAttestation: () => verifyAttestation,
|
|
59
64
|
verifyAttestationSignature: () => verifyAttestationSignature,
|
|
@@ -116,8 +121,62 @@ function computeContentHash(binding, content) {
|
|
|
116
121
|
return `sha256:${sha256Hex(contentCanonicalBytes(binding.kind, content))}`;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
|
-
// src/
|
|
124
|
+
// src/intent-disclosure.ts
|
|
120
125
|
var import_crypto2 = require("crypto");
|
|
126
|
+
function sha256Hex2(bytes) {
|
|
127
|
+
return (0, import_crypto2.createHash)("sha256").update(bytes, "utf8").digest("hex");
|
|
128
|
+
}
|
|
129
|
+
function computeIntentHash(intentText) {
|
|
130
|
+
return `sha256:${sha256Hex2(canonicalizeText(intentText))}`;
|
|
131
|
+
}
|
|
132
|
+
function intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen) {
|
|
133
|
+
const sorted = [...approversFrozen].sort();
|
|
134
|
+
return `${intentCiphertext}
|
|
135
|
+
${canonicalize(sorted)}`;
|
|
136
|
+
}
|
|
137
|
+
function computeIntentDisclosureHash(intentCiphertext, approversFrozen) {
|
|
138
|
+
return `sha256:${sha256Hex2(intentDisclosureCanonicalBytes(intentCiphertext, approversFrozen))}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/identity.ts
|
|
142
|
+
function validateSubject(subject) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
|
|
145
|
+
if (!did) errors.push("subject.did is required");
|
|
146
|
+
if (assurance === "low") {
|
|
147
|
+
if (method !== "self_declared") errors.push(`low assurance requires method "self_declared", got "${method}"`);
|
|
148
|
+
if (trust_root !== "self") errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
|
|
149
|
+
if (disclose) errors.push("low assurance MUST NOT disclose a name");
|
|
150
|
+
} else if (assurance === "high") {
|
|
151
|
+
if (method === "as_vouched") {
|
|
152
|
+
if (trust_root !== "as") errors.push('as_vouched requires trust_root "as"');
|
|
153
|
+
if (!verifier) errors.push("as_vouched requires a verifier");
|
|
154
|
+
} else if (method === "eudi") {
|
|
155
|
+
if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
|
|
156
|
+
if (!verifier) errors.push("eudi requires a verifier");
|
|
157
|
+
if (!owner_signature) errors.push("eudi requires an owner_signature");
|
|
158
|
+
} else {
|
|
159
|
+
errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
|
|
160
|
+
}
|
|
161
|
+
if (disclose && typeof disclose.name !== "string") errors.push("disclose.name must be a string");
|
|
162
|
+
} else {
|
|
163
|
+
errors.push(`assurance must be "low" or "high", got "${assurance}"`);
|
|
164
|
+
}
|
|
165
|
+
return { valid: errors.length === 0, errors };
|
|
166
|
+
}
|
|
167
|
+
function deriveIdentityLine(subject, { operatorName }) {
|
|
168
|
+
const name = subject?.assurance === "high" ? subject.disclose?.name : void 0;
|
|
169
|
+
if (!name) {
|
|
170
|
+
return `Sent by an AI agent via ${operatorName}`;
|
|
171
|
+
}
|
|
172
|
+
if (subject.method === "eudi") {
|
|
173
|
+
return `Sent by an AI agent of ${name} \u2014 identity verified (EUDI)`;
|
|
174
|
+
}
|
|
175
|
+
return `Sent by an AI agent of ${name} \u2014 verified by ${operatorName}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/frame.ts
|
|
179
|
+
var import_crypto3 = require("crypto");
|
|
121
180
|
function validateFrameParams(params, profile) {
|
|
122
181
|
const errors = [];
|
|
123
182
|
if (!profile.frameSchema) {
|
|
@@ -154,7 +213,7 @@ function canonicalFrame(params, profile) {
|
|
|
154
213
|
return lines.join("\n");
|
|
155
214
|
}
|
|
156
215
|
function frameHash(canonicalFrameString) {
|
|
157
|
-
const hash = (0,
|
|
216
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
158
217
|
return `sha256:${hash}`;
|
|
159
218
|
}
|
|
160
219
|
function computeFrameHash(params, profile) {
|
|
@@ -238,17 +297,17 @@ function canonicalContext(params, profile) {
|
|
|
238
297
|
}
|
|
239
298
|
function computeBoundsHash(params, profile) {
|
|
240
299
|
const canonical = canonicalBounds(params, profile);
|
|
241
|
-
const hash = (0,
|
|
300
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
242
301
|
return `sha256:${hash}`;
|
|
243
302
|
}
|
|
244
303
|
function computeContextHash(params, profile) {
|
|
245
304
|
const canonical = canonicalContext(params, profile);
|
|
246
|
-
const hash = (0,
|
|
305
|
+
const hash = (0, import_crypto3.createHash)("sha256").update(canonical, "utf8").digest("hex");
|
|
247
306
|
return `sha256:${hash}`;
|
|
248
307
|
}
|
|
249
308
|
|
|
250
309
|
// src/attestation.ts
|
|
251
|
-
var
|
|
310
|
+
var import_crypto4 = require("crypto");
|
|
252
311
|
var ed = __toESM(require("@noble/ed25519"));
|
|
253
312
|
function decodeAttestationBlob(blob) {
|
|
254
313
|
try {
|
|
@@ -266,7 +325,7 @@ function encodeAttestationBlob(attestation) {
|
|
|
266
325
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
267
326
|
}
|
|
268
327
|
function attestationId(blob) {
|
|
269
|
-
const hash = (0,
|
|
328
|
+
const hash = (0, import_crypto4.createHash)("sha256").update(blob, "utf8").digest("hex");
|
|
270
329
|
return `sha256:${hash}`;
|
|
271
330
|
}
|
|
272
331
|
async function verifyAttestationSignature(attestation, publicKeyHex) {
|
|
@@ -396,11 +455,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
396
455
|
try {
|
|
397
456
|
checkAttestationExpiry(attestation.payload, now);
|
|
398
457
|
} catch {
|
|
399
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
458
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
400
459
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
401
460
|
continue;
|
|
402
461
|
}
|
|
403
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
462
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
404
463
|
coveredDomains.add(rd.domain);
|
|
405
464
|
}
|
|
406
465
|
}
|
|
@@ -475,11 +534,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
475
534
|
try {
|
|
476
535
|
checkAttestationExpiry(attestation.payload, now);
|
|
477
536
|
} catch {
|
|
478
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
537
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
479
538
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
480
539
|
continue;
|
|
481
540
|
}
|
|
482
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
541
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
483
542
|
coveredDomains.add(rd.domain);
|
|
484
543
|
}
|
|
485
544
|
}
|
|
@@ -783,18 +842,23 @@ function resolveCumulativeFields(request, profile, executionLog, now) {
|
|
|
783
842
|
computeContentHash,
|
|
784
843
|
computeContextHash,
|
|
785
844
|
computeFrameHash,
|
|
845
|
+
computeIntentDisclosureHash,
|
|
846
|
+
computeIntentHash,
|
|
786
847
|
contentCanonicalBytes,
|
|
787
848
|
decodeAttestationBlob,
|
|
849
|
+
deriveIdentityLine,
|
|
788
850
|
encodeAttestationBlob,
|
|
789
851
|
frameHash,
|
|
790
852
|
getAllProfiles,
|
|
791
853
|
getProfile,
|
|
854
|
+
intentDisclosureCanonicalBytes,
|
|
792
855
|
isV4Attestation,
|
|
793
856
|
listProfiles,
|
|
794
857
|
registerProfile,
|
|
795
858
|
validateBoundsParams,
|
|
796
859
|
validateContextParams,
|
|
797
860
|
validateFrameParams,
|
|
861
|
+
validateSubject,
|
|
798
862
|
verify,
|
|
799
863
|
verifyAttestation,
|
|
800
864
|
verifyAttestationSignature,
|
package/dist/index.mjs
CHANGED
|
@@ -50,8 +50,62 @@ 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/identity.ts
|
|
71
|
+
function validateSubject(subject) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
|
|
74
|
+
if (!did) errors.push("subject.did is required");
|
|
75
|
+
if (assurance === "low") {
|
|
76
|
+
if (method !== "self_declared") errors.push(`low assurance requires method "self_declared", got "${method}"`);
|
|
77
|
+
if (trust_root !== "self") errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
|
|
78
|
+
if (disclose) errors.push("low assurance MUST NOT disclose a name");
|
|
79
|
+
} else if (assurance === "high") {
|
|
80
|
+
if (method === "as_vouched") {
|
|
81
|
+
if (trust_root !== "as") errors.push('as_vouched requires trust_root "as"');
|
|
82
|
+
if (!verifier) errors.push("as_vouched requires a verifier");
|
|
83
|
+
} else if (method === "eudi") {
|
|
84
|
+
if (trust_root !== "external") errors.push('eudi requires trust_root "external"');
|
|
85
|
+
if (!verifier) errors.push("eudi requires a verifier");
|
|
86
|
+
if (!owner_signature) errors.push("eudi requires an owner_signature");
|
|
87
|
+
} else {
|
|
88
|
+
errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
|
|
89
|
+
}
|
|
90
|
+
if (disclose && typeof disclose.name !== "string") errors.push("disclose.name must be a string");
|
|
91
|
+
} else {
|
|
92
|
+
errors.push(`assurance must be "low" or "high", got "${assurance}"`);
|
|
93
|
+
}
|
|
94
|
+
return { valid: errors.length === 0, errors };
|
|
95
|
+
}
|
|
96
|
+
function deriveIdentityLine(subject, { operatorName }) {
|
|
97
|
+
const name = subject?.assurance === "high" ? subject.disclose?.name : void 0;
|
|
98
|
+
if (!name) {
|
|
99
|
+
return `Sent by an AI agent via ${operatorName}`;
|
|
100
|
+
}
|
|
101
|
+
if (subject.method === "eudi") {
|
|
102
|
+
return `Sent by an AI agent of ${name} \u2014 identity verified (EUDI)`;
|
|
103
|
+
}
|
|
104
|
+
return `Sent by an AI agent of ${name} \u2014 verified by ${operatorName}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/frame.ts
|
|
108
|
+
import { createHash as createHash3 } from "crypto";
|
|
55
109
|
function validateFrameParams(params, profile) {
|
|
56
110
|
const errors = [];
|
|
57
111
|
if (!profile.frameSchema) {
|
|
@@ -88,7 +142,7 @@ function canonicalFrame(params, profile) {
|
|
|
88
142
|
return lines.join("\n");
|
|
89
143
|
}
|
|
90
144
|
function frameHash(canonicalFrameString) {
|
|
91
|
-
const hash =
|
|
145
|
+
const hash = createHash3("sha256").update(canonicalFrameString, "utf8").digest("hex");
|
|
92
146
|
return `sha256:${hash}`;
|
|
93
147
|
}
|
|
94
148
|
function computeFrameHash(params, profile) {
|
|
@@ -172,17 +226,17 @@ function canonicalContext(params, profile) {
|
|
|
172
226
|
}
|
|
173
227
|
function computeBoundsHash(params, profile) {
|
|
174
228
|
const canonical = canonicalBounds(params, profile);
|
|
175
|
-
const hash =
|
|
229
|
+
const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
|
|
176
230
|
return `sha256:${hash}`;
|
|
177
231
|
}
|
|
178
232
|
function computeContextHash(params, profile) {
|
|
179
233
|
const canonical = canonicalContext(params, profile);
|
|
180
|
-
const hash =
|
|
234
|
+
const hash = createHash3("sha256").update(canonical, "utf8").digest("hex");
|
|
181
235
|
return `sha256:${hash}`;
|
|
182
236
|
}
|
|
183
237
|
|
|
184
238
|
// src/attestation.ts
|
|
185
|
-
import { createHash as
|
|
239
|
+
import { createHash as createHash4 } from "crypto";
|
|
186
240
|
import * as ed from "@noble/ed25519";
|
|
187
241
|
function decodeAttestationBlob(blob) {
|
|
188
242
|
try {
|
|
@@ -200,7 +254,7 @@ function encodeAttestationBlob(attestation) {
|
|
|
200
254
|
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
201
255
|
}
|
|
202
256
|
function attestationId(blob) {
|
|
203
|
-
const hash =
|
|
257
|
+
const hash = createHash4("sha256").update(blob, "utf8").digest("hex");
|
|
204
258
|
return `sha256:${hash}`;
|
|
205
259
|
}
|
|
206
260
|
async function verifyAttestationSignature(attestation, publicKeyHex) {
|
|
@@ -330,11 +384,11 @@ async function verifyV3(request, profile, publicKeyHex, now, executionLog, error
|
|
|
330
384
|
try {
|
|
331
385
|
checkAttestationExpiry(attestation.payload, now);
|
|
332
386
|
} catch {
|
|
333
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
387
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
334
388
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
335
389
|
continue;
|
|
336
390
|
}
|
|
337
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
391
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
338
392
|
coveredDomains.add(rd.domain);
|
|
339
393
|
}
|
|
340
394
|
}
|
|
@@ -409,11 +463,11 @@ async function verifyV4(request, profile, publicKeyHex, now, executionLog) {
|
|
|
409
463
|
try {
|
|
410
464
|
checkAttestationExpiry(attestation.payload, now);
|
|
411
465
|
} catch {
|
|
412
|
-
const domainNames = attestation.payload.resolved_domains.map((d) => d.domain).join(", ");
|
|
466
|
+
const domainNames = (attestation.payload.resolved_domains ?? []).map((d) => d.domain).join(", ");
|
|
413
467
|
errors.push({ code: "TTL_EXPIRED", message: `Attestation for domain "${domainNames}" has expired` });
|
|
414
468
|
continue;
|
|
415
469
|
}
|
|
416
|
-
for (const rd of attestation.payload.resolved_domains) {
|
|
470
|
+
for (const rd of attestation.payload.resolved_domains ?? []) {
|
|
417
471
|
coveredDomains.add(rd.domain);
|
|
418
472
|
}
|
|
419
473
|
}
|
|
@@ -716,18 +770,23 @@ export {
|
|
|
716
770
|
computeContentHash,
|
|
717
771
|
computeContextHash,
|
|
718
772
|
computeFrameHash,
|
|
773
|
+
computeIntentDisclosureHash,
|
|
774
|
+
computeIntentHash,
|
|
719
775
|
contentCanonicalBytes,
|
|
720
776
|
decodeAttestationBlob,
|
|
777
|
+
deriveIdentityLine,
|
|
721
778
|
encodeAttestationBlob,
|
|
722
779
|
frameHash,
|
|
723
780
|
getAllProfiles,
|
|
724
781
|
getProfile,
|
|
782
|
+
intentDisclosureCanonicalBytes,
|
|
725
783
|
isV4Attestation,
|
|
726
784
|
listProfiles,
|
|
727
785
|
registerProfile,
|
|
728
786
|
validateBoundsParams,
|
|
729
787
|
validateContextParams,
|
|
730
788
|
validateFrameParams,
|
|
789
|
+
validateSubject,
|
|
731
790
|
verify,
|
|
732
791
|
verifyAttestation,
|
|
733
792
|
verifyAttestationSignature,
|
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/identity.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity Assurance (HAP v0.6) — validation + footer derivation for the signed
|
|
3
|
+
* {@link Subject} overlay. See review.md → "Identity Assurance".
|
|
4
|
+
*
|
|
5
|
+
* Two pure helpers, the single source of truth for both the gateway footer and
|
|
6
|
+
* the AS verify page:
|
|
7
|
+
* - {@link validateSubject} — enforces the level/method/trust-root invariants.
|
|
8
|
+
* - {@link deriveIdentityLine} — the human-readable footer line for a subject.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Subject } from './types';
|
|
12
|
+
|
|
13
|
+
export interface SubjectValidation {
|
|
14
|
+
valid: boolean;
|
|
15
|
+
errors: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Enforce the Identity-Assurance invariants on a single subject:
|
|
20
|
+
* - `self_declared` ⇒ `low` / `self`, no disclosed name.
|
|
21
|
+
* - `as_vouched` ⇒ `high` / `as`, `verifier` required.
|
|
22
|
+
* - `eudi` ⇒ `high` / `external`, `verifier` + `owner_signature` required.
|
|
23
|
+
* - `disclose.name` only at `assurance:"high"`.
|
|
24
|
+
*/
|
|
25
|
+
export function validateSubject(subject: Subject): SubjectValidation {
|
|
26
|
+
const errors: string[] = [];
|
|
27
|
+
const { did, assurance, method, trust_root, verifier, disclose, owner_signature } = subject;
|
|
28
|
+
|
|
29
|
+
if (!did) errors.push('subject.did is required');
|
|
30
|
+
|
|
31
|
+
if (assurance === 'low') {
|
|
32
|
+
if (method !== 'self_declared') errors.push(`low assurance requires method "self_declared", got "${method}"`);
|
|
33
|
+
if (trust_root !== 'self') errors.push(`low assurance requires trust_root "self", got "${trust_root}"`);
|
|
34
|
+
if (disclose) errors.push('low assurance MUST NOT disclose a name');
|
|
35
|
+
} else if (assurance === 'high') {
|
|
36
|
+
if (method === 'as_vouched') {
|
|
37
|
+
if (trust_root !== 'as') errors.push('as_vouched requires trust_root "as"');
|
|
38
|
+
if (!verifier) errors.push('as_vouched requires a verifier');
|
|
39
|
+
} else if (method === 'eudi') {
|
|
40
|
+
if (trust_root !== 'external') errors.push('eudi requires trust_root "external"');
|
|
41
|
+
if (!verifier) errors.push('eudi requires a verifier');
|
|
42
|
+
if (!owner_signature) errors.push('eudi requires an owner_signature');
|
|
43
|
+
} else {
|
|
44
|
+
errors.push(`high assurance requires method "as_vouched" or "eudi", got "${method}"`);
|
|
45
|
+
}
|
|
46
|
+
if (disclose && typeof disclose.name !== 'string') errors.push('disclose.name must be a string');
|
|
47
|
+
} else {
|
|
48
|
+
errors.push(`assurance must be "low" or "high", got "${assurance as string}"`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { valid: errors.length === 0, errors };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The footer line for a subject — the ONE place this wording lives.
|
|
56
|
+
* The name appears only at `high` with a disclosed name; otherwise no name.
|
|
57
|
+
*
|
|
58
|
+
* - low / no disclosure → "Sent by an AI agent via «operator»"
|
|
59
|
+
* - high / as_vouched → "Sent by an AI agent of «name» — verified by «operator»"
|
|
60
|
+
* - high / eudi → "Sent by an AI agent of «name» — identity verified (EUDI)"
|
|
61
|
+
*
|
|
62
|
+
* `operatorName` is the rendered AS operator (e.g. "Suveren") — never hardcode it
|
|
63
|
+
* upstream; a different operator self-vouches under its own name.
|
|
64
|
+
*/
|
|
65
|
+
export function deriveIdentityLine(
|
|
66
|
+
subject: Subject | undefined | null,
|
|
67
|
+
{ operatorName }: { operatorName: string },
|
|
68
|
+
): string {
|
|
69
|
+
const name = subject?.assurance === 'high' ? subject.disclose?.name : undefined;
|
|
70
|
+
if (!name) {
|
|
71
|
+
return `Sent by an AI agent via ${operatorName}`;
|
|
72
|
+
}
|
|
73
|
+
if (subject!.method === 'eudi') {
|
|
74
|
+
return `Sent by an AI agent of ${name} — identity verified (EUDI)`;
|
|
75
|
+
}
|
|
76
|
+
return `Sent by an AI agent of ${name} — verified by ${operatorName}`;
|
|
77
|
+
}
|
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,37 @@ 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;
|
|
54
|
+
/**
|
|
55
|
+
* v0.6 (Identity Assurance) — optional signed overlay carrying the verified
|
|
56
|
+
* real-world identity of the Decision Owner(s), one entry per owner. Present
|
|
57
|
+
* only when identity is disclosed; an attestation with no `subjects` renders
|
|
58
|
+
* as `low` (pseudonymous, no name). See {@link Subject} and
|
|
59
|
+
* {@link deriveIdentityLine}.
|
|
60
|
+
*/
|
|
61
|
+
subjects?: Subject[];
|
|
33
62
|
/**
|
|
34
63
|
* v0.4 — commitment mode chosen by the decision owner at attestation time.
|
|
35
64
|
* - 'automatic': agent may invoke bounded tools without per-action review.
|
|
@@ -44,6 +73,37 @@ export interface AttestationPayload {
|
|
|
44
73
|
expires_at: number;
|
|
45
74
|
}
|
|
46
75
|
|
|
76
|
+
/**
|
|
77
|
+
* v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
|
|
78
|
+
* real-world identity to the attestation, gated by HOW the identity was verified.
|
|
79
|
+
*
|
|
80
|
+
* Two display levels (`assurance`): `low` discloses no name; `high` MAY disclose a
|
|
81
|
+
* name. At `high`, two trust roots: `as` (the AS operator vouches — valid only
|
|
82
|
+
* within its own domain) and `external` (an external eID such as EUDI — carries the
|
|
83
|
+
* owner's own signature, AS-independent). See review.md → "Identity Assurance".
|
|
84
|
+
*/
|
|
85
|
+
export interface Subject {
|
|
86
|
+
/** The Decision Owner DID this subject describes (matches an entry in resolved_owners). */
|
|
87
|
+
did: string;
|
|
88
|
+
/** `low` → no name shown; `high` → the name MAY be shown. */
|
|
89
|
+
assurance: 'low' | 'high';
|
|
90
|
+
/** How identity was established. */
|
|
91
|
+
method: 'self_declared' | 'as_vouched' | 'eudi';
|
|
92
|
+
/** Who vouches: `self` (owner's claim), `as` (operator), `external` (eID scheme). */
|
|
93
|
+
trust_root: 'self' | 'as' | 'external';
|
|
94
|
+
/** Verifier id — the AS operator (as_vouched) or the eID scheme (eudi). Required at `high`. */
|
|
95
|
+
verifier?: string;
|
|
96
|
+
/** Disclosed attributes. `name` present ONLY at `assurance:"high"` and when disclosure is on. */
|
|
97
|
+
disclose?: { name: string };
|
|
98
|
+
/** When the underlying verification was performed (unix seconds). */
|
|
99
|
+
verified_at?: number;
|
|
100
|
+
/**
|
|
101
|
+
* eudi only (Phase 2) — the owner's per-event wallet signature, making the
|
|
102
|
+
* identity claim non-repudiable independent of the AS. Null/absent otherwise.
|
|
103
|
+
*/
|
|
104
|
+
owner_signature?: string | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
47
107
|
export interface Attestation {
|
|
48
108
|
header: AttestationHeader;
|
|
49
109
|
payload: AttestationPayload;
|