@motebit/crypto-appattest 1.0.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.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @motebit/crypto-appattest — Apple App Attest chain-verification
3
+ * adapter for motebit hardware-attestation claims.
4
+ *
5
+ * The metabolic leaf `@motebit/crypto` delegates to when a
6
+ * `HardwareAttestationClaim` declares `platform: "device_check"`.
7
+ * Dep-thin `@motebit/crypto` stays permissive-floor-pure; this package,
8
+ * also on the permissive floor (Apache-2.0), metabolizes `@peculiar/x509`
9
+ * + `cbor2` to judge whether Apple's published CA rooted the leaf that
10
+ * signed the caller's attestation.
11
+ *
12
+ * Wiring from a consumer:
13
+ *
14
+ * ```ts
15
+ * import { verify } from "@motebit/crypto";
16
+ * import { deviceCheckVerifier } from "@motebit/crypto-appattest";
17
+ *
18
+ * const result = await verify(credential, {
19
+ * hardwareAttestation: { deviceCheck: deviceCheckVerifier({ expectedBundleId: "com.motebit.app" }) },
20
+ * });
21
+ * ```
22
+ *
23
+ * This package exports no global state, no side-effect registrations;
24
+ * the injection is call-site only.
25
+ */
26
+ import { verifyAppAttestReceipt } from "./verify.js";
27
+ export { parseAppAttestCbor } from "./cbor.js";
28
+ export { APPLE_APPATTEST_ROOT_PEM, APPLE_APPATTEST_FMT } from "./apple-root.js";
29
+ export { verifyAppAttestReceipt } from "./verify.js";
30
+ /**
31
+ * Factory — build a `deviceCheck` verifier bound to a specific bundle
32
+ * and (optionally) a test root / clock / fmt. The returned function
33
+ * matches the `HardwareAttestationVerifiers.deviceCheck` signature the
34
+ * `@motebit/crypto` dispatcher expects.
35
+ *
36
+ * Signature widened 2026-04-22: the third `context` parameter carries
37
+ * the motebit_id / device_id / attested_at fields the VC subject
38
+ * declares so the verifier can re-derive the JCS body Apple signed
39
+ * over and cryptographically bind it to the caller's Ed25519 identity
40
+ * (see `verify.ts` step 6). Pre-existing call sites that pass only
41
+ * `(claim, expectedIdentityHex)` still compile — the context is
42
+ * optional — but they lose the identity-binding channel and receive
43
+ * `identity_bound: false` in the result.
44
+ *
45
+ * Kept as a factory rather than a free function so the consumer's
46
+ * bundle ID is captured once at wiring time, not repeated at every
47
+ * call site — every subsequent `verify()` call inherits the same
48
+ * binding.
49
+ */
50
+ export function deviceCheckVerifier(config) {
51
+ return async (claim, expectedIdentityHex, context) => {
52
+ const opts = {
53
+ expectedBundleId: config.expectedBundleId,
54
+ expectedIdentityPublicKeyHex: expectedIdentityHex,
55
+ ...(config.rootPem !== undefined ? { rootPem: config.rootPem } : {}),
56
+ ...(config.now !== undefined ? { now: config.now } : {}),
57
+ ...(config.expectedFmt !== undefined ? { expectedFmt: config.expectedFmt } : {}),
58
+ ...(context?.expectedMotebitId !== undefined
59
+ ? { expectedMotebitId: context.expectedMotebitId }
60
+ : {}),
61
+ ...(context?.expectedDeviceId !== undefined
62
+ ? { expectedDeviceId: context.expectedDeviceId }
63
+ : {}),
64
+ ...(context?.expectedAttestedAt !== undefined
65
+ ? { expectedAttestedAt: context.expectedAttestedAt }
66
+ : {}),
67
+ };
68
+ const detail = await verifyAppAttestReceipt(claim, opts);
69
+ return {
70
+ valid: detail.valid,
71
+ platform: "device_check",
72
+ errors: detail.errors,
73
+ attestation_detail: detail,
74
+ };
75
+ };
76
+ }
77
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAKH,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,WAAW,CAAC;AACnE,OAAO,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAgDrD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,mBAAmB,CACjC,MAAiC;IAMjC,OAAO,KAAK,EAAE,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,EAAE;QACnD,MAAM,IAAI,GAA2B;YACnC,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;YACzC,4BAA4B,EAAE,mBAAmB;YACjD,GAAG,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACpE,GAAG,CAAC,MAAM,CAAC,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxD,GAAG,CAAC,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAChF,GAAG,CAAC,OAAO,EAAE,iBAAiB,KAAK,SAAS;gBAC1C,CAAC,CAAC,EAAE,iBAAiB,EAAE,OAAO,CAAC,iBAAiB,EAAE;gBAClD,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,SAAS;gBACzC,CAAC,CAAC,EAAE,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,EAAE;gBAChD,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,OAAO,EAAE,kBAAkB,KAAK,SAAS;gBAC3C,CAAC,CAAC,EAAE,kBAAkB,EAAE,OAAO,CAAC,kBAAkB,EAAE;gBACpD,CAAC,CAAC,EAAE,CAAC;SACR,CAAC;QACF,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACzD,OAAO;YACL,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,cAAc;YACxB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,kBAAkB,EAAE,MAAM;SAC3B,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * App Attest receipt verifier — the core judgment function this
3
+ * package exports.
4
+ *
5
+ * Flow (matches Apple's published verification recipe for App Attest,
6
+ * plus the motebit-specific identity-key binding step):
7
+ *
8
+ * 1. Split the receipt into (attestationObjectBase64,
9
+ * keyIdBase64, clientDataHashBase64).
10
+ * 2. CBOR-decode the attestation object to {fmt, attStmt:{x5c,
11
+ * receipt}, authData}. Assert fmt === "apple-appattest".
12
+ * 3. Parse leaf + intermediate as X.509. Walk the chain from leaf to
13
+ * a self-signed root and verify every signature, every CA bit,
14
+ * every validity window. The terminal cert's DER must equal the
15
+ * pinned Apple App Attest root.
16
+ * 4. Extract OID `1.2.840.113635.100.8.2` from the leaf's
17
+ * extensions. Assert its payload equals SHA256(authData ||
18
+ * clientDataHash) where clientDataHash is the transmitted hash.
19
+ * (In App Attest parlance: the "nonce" binding.)
20
+ * 5. Parse authData (WebAuthn format). First 32 bytes is
21
+ * `rpIdHash`; assert it equals SHA256(bundleId). (The "bundle"
22
+ * binding.)
23
+ * 6. Reconstruct the JCS-canonical attestation body from
24
+ * (motebit_id, device_id, identity_public_key, attested_at) the
25
+ * caller supplies, SHA-256 it, and byte-compare against the
26
+ * transmitted `clientDataHash`. This is the cross-stack binding
27
+ * — without it, every other step would prove only that *some*
28
+ * Apple-attested iOS device did something, not that the Ed25519
29
+ * key the credential subject claims is actually on that device.
30
+ *
31
+ * Apple's own inner `receipt` field is NOT verified here — that
32
+ * requires Apple's server-side refresh endpoint and is out of scope
33
+ * for v1. The outer chain + nonce binding + bundle binding + motebit
34
+ * identity binding is enough for third-party self-verification of
35
+ * device-attested identity.
36
+ */
37
+ import type { HardwareAttestationClaim } from "@motebit/protocol";
38
+ export interface AppAttestVerifyOptions {
39
+ /** iOS bundle identifier that minted the attestation (e.g. "com.motebit.app"). */
40
+ readonly expectedBundleId: string;
41
+ /**
42
+ * Ed25519 identity key (lowercase hex) the motebit VC claims. The
43
+ * attested body MUST name this key.
44
+ */
45
+ readonly expectedIdentityPublicKeyHex: string;
46
+ /**
47
+ * motebit_id from the credential subject. Part of the canonical
48
+ * attestation body the Swift mint path signs; re-derived here and
49
+ * byte-compared against the transmitted clientDataHash so a
50
+ * malicious native client cannot substitute a different body.
51
+ */
52
+ readonly expectedMotebitId?: string;
53
+ /**
54
+ * device_id from the credential subject. Same binding role as
55
+ * `expectedMotebitId`.
56
+ */
57
+ readonly expectedDeviceId?: string;
58
+ /**
59
+ * `attested_at` (unix ms) from the credential subject. Same binding
60
+ * role as `expectedMotebitId`.
61
+ */
62
+ readonly expectedAttestedAt?: number;
63
+ /**
64
+ * Override the pinned Apple root — tests fabricate their own root so
65
+ * chain verification exercises the same code path without needing
66
+ * real Apple-signed leaves. Defaults to the pinned
67
+ * `APPLE_APPATTEST_ROOT_PEM`.
68
+ */
69
+ readonly rootPem?: string;
70
+ /**
71
+ * Clock for chain-validity checks. Defaults to `Date.now`. Tests inject
72
+ * a fixed clock to keep certificate validity windows deterministic.
73
+ */
74
+ readonly now?: () => number;
75
+ /**
76
+ * Override the attestation format check. Defaults to `"apple-appattest"`
77
+ * — the value Apple emits. Exposed only so test fabrications that
78
+ * exercise chain-validation edge cases can still reach the code path.
79
+ */
80
+ readonly expectedFmt?: string;
81
+ }
82
+ export interface AppAttestVerifyError {
83
+ readonly message: string;
84
+ }
85
+ export interface AppAttestVerifyResult {
86
+ readonly valid: boolean;
87
+ readonly cert_chain_valid: boolean;
88
+ readonly nonce_bound: boolean;
89
+ readonly bundle_bound: boolean;
90
+ readonly identity_bound: boolean;
91
+ readonly errors: readonly AppAttestVerifyError[];
92
+ }
93
+ /**
94
+ * Apple App Attest attestation verifier.
95
+ *
96
+ * Pure. No network. No filesystem. Deterministic given `now()`.
97
+ *
98
+ * `claim` is the `HardwareAttestationClaim` as carried inside the
99
+ * motebit AgentTrustCredential. For App Attest, the
100
+ * `attestation_receipt` field is expected to be three base64url
101
+ * segments separated by `.`:
102
+ *
103
+ * `{attestationObjectB64}.{keyIdB64}.{clientDataHashB64}`
104
+ *
105
+ * The mobile mint path constructs this shape; see
106
+ * `apps/mobile/src/mint-hardware-credential.ts`.
107
+ */
108
+ export declare function verifyAppAttestReceipt(claim: HardwareAttestationClaim, opts: AppAttestVerifyOptions): Promise<AppAttestVerifyResult>;
109
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AAKlE,MAAM,WAAW,sBAAsB;IACrC,kFAAkF;IAClF,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,4BAA4B,EAAE,MAAM,CAAC;IAC9C;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,MAAM,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAClD;AAOD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,wBAAwB,EAC/B,IAAI,EAAE,sBAAsB,GAC3B,OAAO,CAAC,qBAAqB,CAAC,CAqMhC"}