@motebit/crypto-android-keystore
v1.1.31
Published
Apache-2.0 verifier for Android Hardware-Backed Keystore Attestation hardware-attestation credentials — offline X.509 chain verification against pinned Google Hardware Attestation roots (RSA-4096 + ECDSA P-384), plus ASN.1 extraction of the KeyDescription
Maintainers
Readme
@motebit/crypto-android-keystore
Offline Apache-2.0 verifier for Android Hardware-Backed Keystore Attestation hardware-attestation credentials.
npm i @motebit/crypto @motebit/crypto-android-keystoreRequirements: ESM-only; Node ≥ 20.
Plugs into @motebit/crypto's HardwareAttestationVerifiers dispatcher as the androidKeystore verifier — called when a credential declares platform: "android_keystore" (Android devices with KeyMaster 3+ / KeyMint 1+ — every modern Android device since Android 7).
Usage
import { verify } from "@motebit/crypto";
import { androidKeystoreVerifier } from "@motebit/crypto-android-keystore";
// Raw DER bytes of the registered Android package's
// `attestationApplicationId`, captured at registration time. Must
// byte-equal what the leaf attestation extension reports.
const expectedAttestationApplicationId: Uint8Array = Uint8Array.from(
Buffer.from("MIGQMYGNMAsEBmNvbS5tb3RlYml0AgEf...", "base64"),
);
const result = await verify(credential, {
hardwareAttestation: {
androidKeystore: androidKeystoreVerifier({ expectedAttestationApplicationId }),
},
});What it verifies
- Cert chain to a pinned Google Hardware Attestation root. Two roots ship pinned: the legacy RSA-4096 root (for factory-provisioned devices) and the modern ECDSA P-384 root (for RKP-provisioned devices). Verifiers MUST pin both — Google rotated from RSA to ECDSA between Feb–Apr 2026, so a verifier pinning only one drops half its install base.
- The Android Key Attestation extension (OID
1.3.6.1.4.1.11129.2.1.17) on the leaf —attestationVersion ≥ 3(Keymaster 3 / Android 7+),attestationSecurityLevel ≥ TRUSTED_ENVIRONMENT(rejects software-only fallback),hardwareEnforced.rootOfTrust.verifiedBootStatein caller's allowlist (default[VERIFIED]),hardwareEnforced.attestationApplicationIdbyte-equals the registered package binding. StrongBox passes and plain TEE passes — the floor isTRUSTED_ENVIRONMENT, and the result surfacesattestation_security_levelso callers can distinguish the two. Today the field is informational — nothing outside this package consumes it yet; a consumer that wants to rank StrongBox above TEE reads it from the result. - Optional revocation snapshot. Caller-supplied snapshot keyed by lowercase-hex serial number, mirroring Google's published shape at
https://android.googleapis.com/attestation/status. Defaults to empty (no revocation enforcement). The verifier never fetches at runtime —@motebit/verifyships an embedded snapshot at release time. - Identity binding. The leaf's
attestationChallengemust byte-equalSHA-256(canonicalJson({ attested_at, device_id, identity_public_key, motebit_id, platform: "android_keystore", version: "1" }))— the same body the Kotlinexpo-android-keystoremint path composes. A malicious client that substitutes any other body fails here.
What a passing verification proves — and what it does not
- Proves a Google-rooted secure-hardware key (TEE or StrongBox) on a verified-boot device running the registered package minted the attestation, and that the challenge names the exact Ed25519 identity key the credential claims.
- Does not prove the certificate is unrevoked today: revocation is only as strong as the caller-supplied snapshot, and the default is empty — no revocation enforcement. Any snapshot is a point-in-time capture, not a live check.
- Does not prove StrongBox: a pass means the
TRUSTED_ENVIRONMENTfloor was met — readattestation_security_levelfrom the result to distinguish. - A passing result raises the credential's hardware-attestation score — additive, never an admission gate. See the hardware-attestation doctrine.
Why pinned
A verifier that dynamically fetched Google's attestation roots has no sovereign story. The pinned roots are the self-attesting contract — third parties audit DEFAULT_ANDROID_KEYSTORE_TRUST_ANCHORS and know which trust anchors this library accepts. Source of truth: roots.json in android/keyattestation, Google's canonical Kotlin reference verifier.
Lower-level primitives
Beyond androidKeystoreVerifier, the package exports the parser + constants + canonical literals for advanced consumers:
verifyAndroidKeystoreAttestation(claim, opts)— bare-metal entry: takes theHardwareAttestationClaimplusAndroidKeystoreVerifyOptionsand returns the structured verification result. Parsing happens inside — the claim's chain is split and theKeyDescriptionextension DER-walked internally; the trust anchors are injected viaopts.rootPems(defaults toDEFAULT_ANDROID_KEYSTORE_TRUST_ANCHORS).androidKeystoreVerifieris a thin curry over this.parseKeyDescription(derBytes)— walk the AOSPKeyDescriptionASN.1 extension into a typed structure (attestationVersion,attestationSecurityLevel,hardwareEnforced, etc.).SECURITY_LEVEL_SOFTWARE,SECURITY_LEVEL_TRUSTED_ENVIRONMENT,SECURITY_LEVEL_STRONG_BOX— the canonicalattestationSecurityLevelenum values per AOSP. Use these to constrain the accepted floor.VERIFIED_BOOT_STATE_VERIFIED,VERIFIED_BOOT_STATE_SELF_SIGNED,VERIFIED_BOOT_STATE_UNVERIFIED,VERIFIED_BOOT_STATE_FAILED— the four canonicalverifiedBootStatevalues; populateallowedVerifiedBootStatesfrom this set.ANDROID_KEYSTORE_PLATFORM— the canonical platform-string constant ("android_keystore").ANDROID_KEY_ATTESTATION_OID(1.3.6.1.4.1.11129.2.1.17) — the X.509 extension OID the leaf carries.GOOGLE_ANDROID_KEYSTORE_ROOT_RSA_PEM,GOOGLE_ANDROID_KEYSTORE_ROOT_ECDSA_PEM— the two pinned Google attestation roots (RSA-4096 + ECDSA P-384). Both ship by default; overrideable viaHardwareVerifierBundleConfig.androidKeystoreRootPemsin@motebit/verify.DEFAULT_ANDROID_KEYSTORE_TRUST_ANCHORS— the default accept-set (both pinned roots), exported for audit and as therootPemsdefault inverifyAndroidKeystoreAttestation.EMPTY_REVOCATION_SNAPSHOT— typed empty snapshot for callers that don't yet wire a revocation list. Replace with a real snapshot at release time per@motebit/verify's embedding pipeline.
Why a hand-rolled DER walker
The KeyDescription ASN.1 structure has ~50 optional context-tagged fields in AuthorizationList, two of which carry policy-relevant material ([704] rootOfTrust and [709] attestationApplicationId). A schema-driven parser would have to declare all 50 fields just to skip past the ones we ignore. Walking the DER directly costs ~150 lines and stays scoped to exactly what verification needs — same trade-off @motebit/crypto-tpm made for TPMS_ATTEST parsing.
Privacy posture
Closer to FIDO Yubico-batch than to TPM EK. The leaf X.509 subject is the fixed string CN=Android Keystore Key — not device-identifying. The optional ID-attestation family (attestationIdSerial, attestationIdImei, etc.) only fires when the caller invokes setDevicePropertiesAttestationIncluded(true); motebit does not. Default setAttestationChallenge() produces batch-shareable chains with the device-identifying material confined to (a) the caller-controlled challenge and (b) verifiedBootKey (boot-image identity, not user identity).
Related
@motebit/crypto— dispatcher (pure permissive-floor; zero deps)@motebit/crypto-appattest— iOS sibling@motebit/crypto-tpm— Windows / Linux TPM sibling@motebit/crypto-webauthn— browser sibling@motebit/verify— canonical CLI bundling the platform leaves with motebit defaults
License
Apache-2.0 — see LICENSE and NOTICE.
"Motebit" is a trademark. The Apache License grants rights to this software, not to any Motebit trademarks, logos, or branding. You may not use Motebit branding in a way that suggests endorsement or affiliation without written permission.
