@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/LICENSE +206 -0
- package/NOTICE +19 -0
- package/README.md +48 -0
- package/dist/apple-root.d.ts +41 -0
- package/dist/apple-root.d.ts.map +1 -0
- package/dist/apple-root.js +55 -0
- package/dist/apple-root.js.map +1 -0
- package/dist/cbor.d.ts +32 -0
- package/dist/cbor.d.ts.map +1 -0
- package/dist/cbor.js +88 -0
- package/dist/cbor.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/verify.d.ts +109 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +536 -0
- package/dist/verify.js.map +1 -0
- package/package.json +80 -0
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"}
|
package/dist/verify.d.ts
ADDED
|
@@ -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"}
|