@humanagencyp/hap-core 0.8.3 → 0.9.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/README.md +4 -4
- package/dist/index.d.mts +258 -9
- package/dist/index.d.ts +258 -9
- package/dist/index.js +227 -5
- package/dist/index.mjs +213 -4
- package/package.json +3 -2
- package/src/did-key.ts +126 -0
- package/src/identity.ts +21 -5
- package/src/index.ts +3 -0
- package/src/mandate.ts +207 -0
- package/src/receipt.ts +74 -0
- package/src/types.ts +81 -5
package/src/receipt.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution receipts (HAP v0.5/v0.6) — the wire type and holder-side
|
|
3
|
+
* verification. Until v0.6 this library had no receipt surface at all; every
|
|
4
|
+
* verifier hand-assembled the strip→canonicalize→verify dance. This module is
|
|
5
|
+
* that dance, once.
|
|
6
|
+
*
|
|
7
|
+
* Receipt payloads are camelCase on the wire (unlike the snake_case
|
|
8
|
+
* attestation payload) — a shipped inconsistency v0.6 chose to document
|
|
9
|
+
* rather than break (changelog.md → "Renames and terminology").
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as ed from '@noble/ed25519';
|
|
13
|
+
import type { ContentBinding, Subject } from './types';
|
|
14
|
+
import { canonicalize } from './canonicalize';
|
|
15
|
+
|
|
16
|
+
/** Signed execution receipt — protocol.md → "Receipt Payload Schema". */
|
|
17
|
+
export interface ReceiptPayload {
|
|
18
|
+
id: string;
|
|
19
|
+
groupId?: string | null;
|
|
20
|
+
userId?: string;
|
|
21
|
+
boundsHash: string;
|
|
22
|
+
profileId: string;
|
|
23
|
+
/** Downstream tool name. Audit metadata only — never a dispatch key. */
|
|
24
|
+
action: string;
|
|
25
|
+
/** Semantic category — drives cumulative bucketing and bounds dispatch. */
|
|
26
|
+
actionType: string;
|
|
27
|
+
executionContext: Record<string, unknown>;
|
|
28
|
+
cumulativeState?: Record<string, unknown>;
|
|
29
|
+
limits?: Record<string, unknown>;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
/** v0.6 — hash of the bound content, computed by the Gatekeeper, copied
|
|
32
|
+
* verbatim by the AS. The AS never sees the content. */
|
|
33
|
+
contentHash?: string;
|
|
34
|
+
/** v0.6 — the binding declaration echoed into the signed receipt, so a
|
|
35
|
+
* verifier knows exactly what `contentHash` covers. */
|
|
36
|
+
contentBinding?: Pick<ContentBinding, 'version' | 'kind' | 'fields' | 'required_fields' | 'appliesTo'>;
|
|
37
|
+
/** v0.6 — disclosed subset of the attestation's identity overlay. */
|
|
38
|
+
subjects?: Subject[];
|
|
39
|
+
/** v0.6 — review path: the proposal this receipt executed. */
|
|
40
|
+
proposalId?: string;
|
|
41
|
+
/** v0.6 — review path: the owner's HAP-approval signature (or its sha256)
|
|
42
|
+
* for the executed proposal. */
|
|
43
|
+
approvalSignature?: string;
|
|
44
|
+
/** Ed25519 over the JCS-canonical payload (this field excluded), base64url. */
|
|
45
|
+
signature: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Verify a receipt's AS signature: strip `signature`, JCS-canonicalize the
|
|
50
|
+
* rest, Ed25519-verify against the AS public key (hex). This is holder-side
|
|
51
|
+
* verification — it needs the COMPLETE receipt; a redacted public view cannot
|
|
52
|
+
* be re-verified this way (protocol.md → "Receipt Verification").
|
|
53
|
+
*
|
|
54
|
+
* Content is checked separately: where `contentHash` is present, recompute it
|
|
55
|
+
* from the held artifact via the content-binding module and compare.
|
|
56
|
+
*
|
|
57
|
+
* @throws Error prefixed `INVALID_SIGNATURE:` when verification fails.
|
|
58
|
+
*/
|
|
59
|
+
export async function verifyReceiptSignature(receipt: ReceiptPayload, publicKeyHex: string): Promise<void> {
|
|
60
|
+
const { signature, ...unsigned } = receipt;
|
|
61
|
+
if (!signature) {
|
|
62
|
+
throw new Error('INVALID_SIGNATURE: receipt carries no signature');
|
|
63
|
+
}
|
|
64
|
+
const bytes = new TextEncoder().encode(canonicalize(unsigned));
|
|
65
|
+
const base64 = signature.replace(/-/g, '+').replace(/_/g, '/');
|
|
66
|
+
const padding = base64.length % 4 === 0 ? '' : '='.repeat(4 - (base64.length % 4));
|
|
67
|
+
const sigBytes = new Uint8Array(Buffer.from(base64 + padding, 'base64'));
|
|
68
|
+
const publicKeyBytes = new Uint8Array(Buffer.from(publicKeyHex, 'hex'));
|
|
69
|
+
|
|
70
|
+
const ok = await ed.verifyAsync(sigBytes, bytes, publicKeyBytes).catch(() => false);
|
|
71
|
+
if (!ok) {
|
|
72
|
+
throw new Error('INVALID_SIGNATURE: receipt signature verification failed');
|
|
73
|
+
}
|
|
74
|
+
}
|
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' | '0.5';
|
|
22
|
+
version: '0.3' | '0.4' | '0.5' | '0.6';
|
|
23
23
|
profile_id: string;
|
|
24
24
|
/** v0.3 (deprecated) — hash of the authorization frame */
|
|
25
25
|
frame_hash?: string;
|
|
@@ -67,12 +67,55 @@ export interface AttestationPayload {
|
|
|
67
67
|
*
|
|
68
68
|
* Cryptographically bound in the signed payload so a compromised SP cannot
|
|
69
69
|
* silently flip 'review' → 'automatic'. Absent on v0.3 attestations.
|
|
70
|
+
* `review_above_cap` (v0.5): automatic below the signed caps, proposal above.
|
|
70
71
|
*/
|
|
71
|
-
commitment_mode?: 'automatic' | 'review';
|
|
72
|
+
commitment_mode?: 'automatic' | 'review' | 'review_above_cap';
|
|
73
|
+
/**
|
|
74
|
+
* v0.6 (Owner Mandate Signatures) — the Decision Owner's own signature(s)
|
|
75
|
+
* over the mandate projection ({@link MandateProjection}), one entry per
|
|
76
|
+
* co-signing owner. Carried INSIDE the AS-signed payload, so the AS attests
|
|
77
|
+
* to having received it and cannot strip it without invalidating its own
|
|
78
|
+
* signature. Optional and additive: an attestation without it behaves as
|
|
79
|
+
* before (the v0.5 posture — the AS asserts, nobody co-signs).
|
|
80
|
+
*/
|
|
81
|
+
owner_mandates?: OwnerMandate[];
|
|
72
82
|
issued_at: number;
|
|
73
83
|
expires_at: number;
|
|
74
84
|
}
|
|
75
85
|
|
|
86
|
+
/** Mandate-assurance axis — what custody signed the mandate. Independent of
|
|
87
|
+
* identity assurance ({@link Subject.method}). */
|
|
88
|
+
export type MandateBinding = 'raw' | 'webauthn' | 'eudi';
|
|
89
|
+
|
|
90
|
+
/** Which surface showed the owner what they signed. A DECLARATION, not a proof
|
|
91
|
+
* — no verifier can check it; see protocol.md → Owner Mandate Signatures. */
|
|
92
|
+
export type SigningSurface = 'gatekeeper_local' | 'as_web' | 'wallet_display';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* v0.6 — one owner's signature over the mandate projection.
|
|
96
|
+
*
|
|
97
|
+
* There is deliberately NO `public_key` field, and it MUST NOT be added: the
|
|
98
|
+
* verification key is carried in the DID itself (key-bearing `did:key`), so a
|
|
99
|
+
* non-key-bearing DID fails STRUCTURALLY instead of validating against a key
|
|
100
|
+
* the AS could have substituted. `alg` is kept only as curve-confusion
|
|
101
|
+
* hygiene; if it disagrees with the DID's key type, the DID is authoritative.
|
|
102
|
+
*/
|
|
103
|
+
export interface OwnerMandate {
|
|
104
|
+
/** The signing owner's key-bearing DID. MUST be a member of `resolved_owners`. */
|
|
105
|
+
did: string;
|
|
106
|
+
/** Signature algorithm hint. The DID's multicodec wins on any disagreement. */
|
|
107
|
+
alg: 'EdDSA' | 'ES256';
|
|
108
|
+
/** base64url (no padding) signature over the JCS bytes of the mandate projection. */
|
|
109
|
+
signature: string;
|
|
110
|
+
/** When the owner signed (unix seconds). */
|
|
111
|
+
signed_at: number;
|
|
112
|
+
/** Defence-in-depth against duplicate issuance by an HONEST AS only —
|
|
113
|
+
* AS-side nonce enforcement is no defence against the AS itself. */
|
|
114
|
+
nonce: string;
|
|
115
|
+
binding: MandateBinding;
|
|
116
|
+
signing_surface?: SigningSurface;
|
|
117
|
+
}
|
|
118
|
+
|
|
76
119
|
/**
|
|
77
120
|
* v0.6 Identity Assurance — a signed overlay binding a Decision Owner's verified
|
|
78
121
|
* real-world identity to the attestation, gated by HOW the identity was verified.
|
|
@@ -98,8 +141,11 @@ export interface Subject {
|
|
|
98
141
|
/** When the underlying verification was performed (unix seconds). */
|
|
99
142
|
verified_at?: number;
|
|
100
143
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
144
|
+
* @deprecated v0.6 — replaced by {@link AttestationPayload.owner_mandates}.
|
|
145
|
+
* This field signed the wrong object (the identity claim, not what was
|
|
146
|
+
* committed to) and was welded to one method. Implementations MUST NOT emit
|
|
147
|
+
* it; verifiers MAY ignore it on artifacts that carry it. Kept readable for
|
|
148
|
+
* audit of pre-0.6 artifacts only.
|
|
103
149
|
*/
|
|
104
150
|
owner_signature?: string | null;
|
|
105
151
|
}
|
|
@@ -340,6 +386,15 @@ export interface ProfileContextField {
|
|
|
340
386
|
description?: string;
|
|
341
387
|
displayName?: string;
|
|
342
388
|
format?: 'email' | 'domain' | 'url' | 'currency';
|
|
389
|
+
/**
|
|
390
|
+
* v0.6 — what this field NAMES on the read path. `counterparty` = the other
|
|
391
|
+
* party to a communication (matched against an item's participants);
|
|
392
|
+
* `resource` = the container an item belongs to (a direct attribute match —
|
|
393
|
+
* and a resource scope enforced on writes MUST also bind reads). Absent →
|
|
394
|
+
* implementations MAY infer `counterparty` from `format: email|domain`
|
|
395
|
+
* (transitional; the explicit declaration is normative).
|
|
396
|
+
*/
|
|
397
|
+
scopeKind?: 'counterparty' | 'resource';
|
|
343
398
|
constraint?: FieldConstraint;
|
|
344
399
|
enum?: string[];
|
|
345
400
|
}
|
|
@@ -636,7 +691,28 @@ export interface GatekeeperRequest {
|
|
|
636
691
|
* Structured error from Gatekeeper verification.
|
|
637
692
|
*/
|
|
638
693
|
export interface GatekeeperError {
|
|
639
|
-
code:
|
|
694
|
+
code:
|
|
695
|
+
| 'BOUND_EXCEEDED'
|
|
696
|
+
| 'CUMULATIVE_LIMIT_EXCEEDED'
|
|
697
|
+
| 'INVALID_SIGNATURE'
|
|
698
|
+
| 'TTL_EXPIRED'
|
|
699
|
+
| 'FRAME_MISMATCH'
|
|
700
|
+
| 'BOUNDS_MISMATCH'
|
|
701
|
+
| 'CONTEXT_MISMATCH'
|
|
702
|
+
| 'DOMAIN_NOT_COVERED'
|
|
703
|
+
| 'INVALID_PROFILE'
|
|
704
|
+
| 'MALFORMED_ATTESTATION'
|
|
705
|
+
// v0.5/v0.6 receipt-path codes a Gatekeeper must recognize as definitive
|
|
706
|
+
// rejections (protocol.md → Pre-flight Receipt Request):
|
|
707
|
+
| 'INVALID_ACTION_TYPE'
|
|
708
|
+
| 'ATTESTATION_REVOKED'
|
|
709
|
+
| 'APPROVAL_REQUIRED'
|
|
710
|
+
| 'IDEMPOTENCY_MISMATCH'
|
|
711
|
+
// v0.6 owner-mandate codes (protocol.md → Owner Mandate Signatures):
|
|
712
|
+
| 'MANDATE_SIGNATURE_REQUIRED'
|
|
713
|
+
| 'MANDATE_SIGNATURE_INVALID'
|
|
714
|
+
| 'APPROVAL_SIGNATURE_REQUIRED'
|
|
715
|
+
| 'APPROVAL_SIGNATURE_INVALID';
|
|
640
716
|
field?: string;
|
|
641
717
|
message: string;
|
|
642
718
|
bound?: string | number;
|