@myazahq/kyc-sdk-react-native 2.2.0 → 2.3.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/package.json +1 -1
- package/src/components/KycSheet.tsx +209 -127
- package/src/components/MyazaInput.tsx +6 -1
- package/src/components/MyazaSelect.tsx +5 -0
- package/src/components/PoweredBy.tsx +20 -15
- package/src/components/ProgressBar.tsx +91 -0
- package/src/components/StepIndicator.tsx +145 -31
- package/src/components/fonts.ts +23 -0
- package/src/config/questionnaire.ts +45 -4
- package/src/config/workflowMerge.ts +1 -0
- package/src/emrtd/crypto.ts +1 -1
- package/src/emrtd/dg1.ts +55 -0
- package/src/emrtd/ec-curves.ts +142 -0
- package/src/emrtd/ec.ts +196 -0
- package/src/emrtd/index.ts +1 -0
- package/src/emrtd/mrzKey.ts +21 -1
- package/src/emrtd/open.ts +169 -0
- package/src/emrtd/pace-params.ts +169 -0
- package/src/emrtd/pace.ts +295 -0
- package/src/emrtd/secureMessaging.ts +32 -14
- package/src/emrtd/session.ts +124 -20
- package/src/emrtd/suites.ts +99 -0
- package/src/index.ts +1 -0
- package/src/lib/step-log.ts +43 -0
- package/src/lib/step-window.ts +96 -0
- package/src/liveness/useLiveness.ts +1 -1
- package/src/screens/IdTypeStep.tsx +15 -2
- package/src/screens/NfcStep.tsx +12 -0
- package/src/screens/QuestionnaireField.tsx +30 -0
- package/src/screens/QuestionnaireStep.tsx +3 -1
- package/src/screens/nfc/NfcSuccessPanel.tsx +13 -6
- package/src/services/deviceMetadata.ts +9 -1
- package/src/store/derive.ts +7 -0
- package/src/store/kycStore.ts +25 -1
- package/src/types/config.ts +17 -0
- package/src/types/workflow.ts +10 -0
package/src/emrtd/ec.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { EcCurve } from './ec-curves';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Elliptic-curve point arithmetic over a prime field, in BigInt.
|
|
5
|
+
//
|
|
6
|
+
// WHY THIS IS IN TYPESCRIPT while the block ciphers are native. The native side
|
|
7
|
+
// exposes 3DES/AES/SHA/CMAC because those are audited, constant-time, hardware
|
|
8
|
+
// -accelerated platform primitives. It exposes no EC point arithmetic, and
|
|
9
|
+
// adding it would mean writing and shipping matching Swift and Kotlin — two
|
|
10
|
+
// more implementations of the same maths, on the path that authenticates a
|
|
11
|
+
// passport. One implementation, checked against the curve's own published
|
|
12
|
+
// generator and order (ec.test.ts), is the smaller risk.
|
|
13
|
+
//
|
|
14
|
+
// The scalar multiply is a fixed-sequence Montgomery ladder: every bit of the
|
|
15
|
+
// scalar performs the same add-then-double regardless of its value, so the
|
|
16
|
+
// operation sequence does not depend on the secret. That is as far as constant
|
|
17
|
+
// time goes here — BigInt itself is variable-time, so this is not hardened
|
|
18
|
+
// against a local timing attacker. It does not need to be: the private scalar
|
|
19
|
+
// is ephemeral, used for exactly one handshake with a chip held against the
|
|
20
|
+
// phone, and discarded.
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** An affine point, or the point at infinity (`inf`). */
|
|
24
|
+
export interface EcPoint {
|
|
25
|
+
readonly x: bigint;
|
|
26
|
+
readonly y: bigint;
|
|
27
|
+
readonly inf: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const INFINITY: EcPoint = { x: 0n, y: 0n, inf: true };
|
|
31
|
+
|
|
32
|
+
export function point(x: bigint, y: bigint): EcPoint {
|
|
33
|
+
return { x, y, inf: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Least non-negative residue — `%` alone keeps the sign of the dividend. */
|
|
37
|
+
function mod(a: bigint, p: bigint): bigint {
|
|
38
|
+
const r = a % p;
|
|
39
|
+
return r < 0n ? r + p : r;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Modular inverse by the extended Euclidean algorithm.
|
|
44
|
+
*
|
|
45
|
+
* Throws when the value is not invertible, which for a prime field means it was
|
|
46
|
+
* congruent to zero. Callers reach this only through point arithmetic that has
|
|
47
|
+
* already excluded the cases where that happens.
|
|
48
|
+
*/
|
|
49
|
+
export function modInverse(value: bigint, p: bigint): bigint {
|
|
50
|
+
let [old_r, r] = [mod(value, p), p];
|
|
51
|
+
let [old_s, s] = [1n, 0n];
|
|
52
|
+
while (r !== 0n) {
|
|
53
|
+
const q = old_r / r;
|
|
54
|
+
[old_r, r] = [r, old_r - q * r];
|
|
55
|
+
[old_s, s] = [s, old_s - q * s];
|
|
56
|
+
}
|
|
57
|
+
if (old_r !== 1n) throw new Error('value is not invertible');
|
|
58
|
+
return mod(old_s, p);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether the point satisfies y² = x³ + ax + b over the curve's field. */
|
|
62
|
+
export function isOnCurve(c: EcCurve, pt: EcPoint): boolean {
|
|
63
|
+
if (pt.inf) return false;
|
|
64
|
+
if (pt.x < 0n || pt.x >= c.p || pt.y < 0n || pt.y >= c.p) return false;
|
|
65
|
+
const lhs = mod(pt.y * pt.y, c.p);
|
|
66
|
+
const rhs = mod(pt.x * pt.x * pt.x + c.a * pt.x + c.b, c.p);
|
|
67
|
+
return lhs === rhs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function pointDouble(c: EcCurve, pt: EcPoint): EcPoint {
|
|
71
|
+
if (pt.inf) return INFINITY;
|
|
72
|
+
// A point with y = 0 is its own inverse, so doubling it lands on infinity.
|
|
73
|
+
if (pt.y === 0n) return INFINITY;
|
|
74
|
+
const lambda = mod(
|
|
75
|
+
(3n * pt.x * pt.x + c.a) * modInverse(2n * pt.y, c.p),
|
|
76
|
+
c.p,
|
|
77
|
+
);
|
|
78
|
+
const x = mod(lambda * lambda - 2n * pt.x, c.p);
|
|
79
|
+
return point(x, mod(lambda * (pt.x - x) - pt.y, c.p));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function pointAdd(c: EcCurve, a: EcPoint, b: EcPoint): EcPoint {
|
|
83
|
+
if (a.inf) return b;
|
|
84
|
+
if (b.inf) return a;
|
|
85
|
+
if (a.x === b.x) {
|
|
86
|
+
// Same x: either the same point (double) or inverses (sum is infinity).
|
|
87
|
+
return mod(a.y + b.y, c.p) === 0n ? INFINITY : pointDouble(c, a);
|
|
88
|
+
}
|
|
89
|
+
const lambda = mod((b.y - a.y) * modInverse(b.x - a.x, c.p), c.p);
|
|
90
|
+
const x = mod(lambda * lambda - a.x - b.x, c.p);
|
|
91
|
+
return point(x, mod(lambda * (a.x - x) - a.y, c.p));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scalar multiplication by a Montgomery ladder.
|
|
96
|
+
*
|
|
97
|
+
* Both branches of every bit do the same work, so the sequence of field
|
|
98
|
+
* operations is independent of the scalar's bits (see the header note on how
|
|
99
|
+
* far that guarantee goes). The ladder starts at the scalar's own bit width
|
|
100
|
+
* rather than the curve's, which is safe here because the scalar is uniformly
|
|
101
|
+
* random in [1, n-1] and never a low-entropy value whose bit length would leak
|
|
102
|
+
* something meaningful.
|
|
103
|
+
*/
|
|
104
|
+
export function pointMultiply(c: EcCurve, k: bigint, pt: EcPoint): EcPoint {
|
|
105
|
+
if (k <= 0n || pt.inf) return INFINITY;
|
|
106
|
+
let r0: EcPoint = INFINITY;
|
|
107
|
+
let r1: EcPoint = pt;
|
|
108
|
+
for (let i = BigInt(k.toString(2).length) - 1n; i >= 0n; i--) {
|
|
109
|
+
if (((k >> i) & 1n) === 0n) {
|
|
110
|
+
r1 = pointAdd(c, r0, r1);
|
|
111
|
+
r0 = pointDouble(c, r0);
|
|
112
|
+
} else {
|
|
113
|
+
r0 = pointAdd(c, r0, r1);
|
|
114
|
+
r1 = pointDouble(c, r1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return r0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The curve's generator as a point. */
|
|
121
|
+
export function generator(c: EcCurve): EcPoint {
|
|
122
|
+
return point(c.gx, c.gy);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Encoding ────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** Big-endian bytes → integer. */
|
|
128
|
+
export function bytesToBigInt(bytes: Uint8Array): bigint {
|
|
129
|
+
let n = 0n;
|
|
130
|
+
for (const b of bytes) n = (n << 8n) | BigInt(b);
|
|
131
|
+
return n;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Integer → fixed-width big-endian bytes.
|
|
136
|
+
*
|
|
137
|
+
* The width is fixed on purpose: a shared secret whose x coordinate happens to
|
|
138
|
+
* start with a zero byte must still derive the same-length key material, and a
|
|
139
|
+
* variable-length encoding there silently produces the wrong session keys on
|
|
140
|
+
* roughly one handshake in 256.
|
|
141
|
+
*/
|
|
142
|
+
export function bigIntToBytes(value: bigint, width: number): Uint8Array {
|
|
143
|
+
const out = new Uint8Array(width);
|
|
144
|
+
let v = value;
|
|
145
|
+
for (let i = width - 1; i >= 0 && v > 0n; i--) {
|
|
146
|
+
out[i] = Number(v & 0xffn);
|
|
147
|
+
v >>= 8n;
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Uncompressed point encoding: 0x04 || X || Y, each coordinate field-width. */
|
|
153
|
+
export function encodePoint(c: EcCurve, pt: EcPoint): Uint8Array {
|
|
154
|
+
if (pt.inf) throw new Error('cannot encode the point at infinity');
|
|
155
|
+
const out = new Uint8Array(1 + c.byteLen * 2);
|
|
156
|
+
out[0] = 0x04;
|
|
157
|
+
out.set(bigIntToBytes(pt.x, c.byteLen), 1);
|
|
158
|
+
out.set(bigIntToBytes(pt.y, c.byteLen), 1 + c.byteLen);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Decode an uncompressed point, returning null when the bytes are not a valid
|
|
164
|
+
* point on the curve.
|
|
165
|
+
*
|
|
166
|
+
* Validation is the security-critical part, not a formality: a chip that picks
|
|
167
|
+
* a point off the curve (or a small-order point on a related curve) can learn
|
|
168
|
+
* our private scalar from how we respond, so anything that does not verify is
|
|
169
|
+
* refused rather than used. Compressed encodings are rejected too, since PACE
|
|
170
|
+
* mandates the uncompressed form and accepting more would only widen what an
|
|
171
|
+
* attacker can hand us.
|
|
172
|
+
*/
|
|
173
|
+
export function decodePoint(c: EcCurve, bytes: Uint8Array): EcPoint | null {
|
|
174
|
+
if (bytes.length !== 1 + c.byteLen * 2 || bytes[0] !== 0x04) return null;
|
|
175
|
+
const pt = point(
|
|
176
|
+
bytesToBigInt(bytes.subarray(1, 1 + c.byteLen)),
|
|
177
|
+
bytesToBigInt(bytes.subarray(1 + c.byteLen)),
|
|
178
|
+
);
|
|
179
|
+
return isOnCurve(c, pt) ? pt : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* A uniformly random private scalar in [1, n-1].
|
|
184
|
+
*
|
|
185
|
+
* Rejection sampling rather than a modulo: reducing a random integer mod n
|
|
186
|
+
* biases the result toward small values, and biased nonces are how ECDSA keys
|
|
187
|
+
* have historically been recovered. The loop is bounded because each draw
|
|
188
|
+
* succeeds with probability very close to 1 for every curve here.
|
|
189
|
+
*/
|
|
190
|
+
export function randomScalar(c: EcCurve, randomBytes: (n: number) => Uint8Array): bigint {
|
|
191
|
+
for (let attempt = 0; attempt < 64; attempt++) {
|
|
192
|
+
const candidate = bytesToBigInt(randomBytes(c.byteLen));
|
|
193
|
+
if (candidate > 0n && candidate < c.n) return candidate;
|
|
194
|
+
}
|
|
195
|
+
throw new Error('could not draw a private scalar');
|
|
196
|
+
}
|
package/src/emrtd/index.ts
CHANGED
|
@@ -26,5 +26,6 @@ export {
|
|
|
26
26
|
nfcStageLabel,
|
|
27
27
|
nfcStageProgress,
|
|
28
28
|
} from './stages';
|
|
29
|
+
export { parseDg1 } from './dg1';
|
|
29
30
|
export { decodeChipImage, isNfcAvailable, nfcUnavailableReason } from './native';
|
|
30
31
|
export { cancelChipRead, readPassportChip, type ChipReadOptions } from './read';
|
package/src/emrtd/mrzKey.ts
CHANGED
|
@@ -48,7 +48,27 @@ export function mrzKeySeedInput(fields: MrzKeyFields): string {
|
|
|
48
48
|
);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
/** Kseed: the first 16 bytes of SHA-1 over the MRZ information. */
|
|
51
|
+
/** Kseed for BAC: the first 16 bytes of SHA-1 over the MRZ information. */
|
|
52
52
|
export function keySeed(p: EmrtdPrimitives, fields: MrzKeyFields): Uint8Array {
|
|
53
53
|
return p.sha1(fromAscii(mrzKeySeedInput(fields))).subarray(0, 16);
|
|
54
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The seed PACE derives its password key from: the FULL SHA-1 digest, NOT
|
|
58
|
+
* truncated to 16 bytes.
|
|
59
|
+
*
|
|
60
|
+
* This is the one place the two protocols disagree about the same hash, and it
|
|
61
|
+
* is silent when wrong: a truncated seed yields a valid-looking K_π, the
|
|
62
|
+
* handshake runs to its last step, and the chip answers 0x6300 — which reads
|
|
63
|
+
* exactly like a mistyped MRZ rather than a derivation bug. It cost this SDK a
|
|
64
|
+
* real passport read to find (the same document opened over BAC moments later,
|
|
65
|
+
* proving the MRZ was right).
|
|
66
|
+
*
|
|
67
|
+
* ICAO 9303 Part 11 keeps the two apart: §9.7.3 truncates for BAC's Kseed,
|
|
68
|
+
* while §9.7.2 feeds the whole digest to KDF_π. JMRTD encodes the same split as
|
|
69
|
+
* `computeKeySeedForBAC(… truncate: true)` vs `computeKeySeedForPACE(…
|
|
70
|
+
* truncate: false)`.
|
|
71
|
+
*/
|
|
72
|
+
export function paceKeySeed(p: EmrtdPrimitives, fields: MrzKeyFields): Uint8Array {
|
|
73
|
+
return p.sha1(fromAscii(mrzKeySeedInput(fields)));
|
|
74
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { fromBase64, toBase64 } from './bytes';
|
|
2
|
+
import type { EmrtdPrimitives, MrzKeyFields } from './crypto';
|
|
3
|
+
import { paceKeySeed } from './crypto';
|
|
4
|
+
import { curveForParameterId } from './ec-curves';
|
|
5
|
+
import { runPaceEcdhGm, PaceError, type PaceTransceive } from './pace';
|
|
6
|
+
import {
|
|
7
|
+
describeProtocol,
|
|
8
|
+
paceGapFor,
|
|
9
|
+
parseCardAccess,
|
|
10
|
+
selectPaceOffer,
|
|
11
|
+
} from './pace-params';
|
|
12
|
+
import type { SecureMessagingSession } from './secureMessaging';
|
|
13
|
+
import type { EmrtdTransport } from './session';
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Choosing how to get into the chip.
|
|
17
|
+
//
|
|
18
|
+
// A chip may accept BAC, PACE, or both, and this decides which is tried first.
|
|
19
|
+
//
|
|
20
|
+
// The standard prefers PACE, and so should we eventually: BAC derives its keys
|
|
21
|
+
// from the MRZ alone, so anyone who photographs the passport page can decrypt a
|
|
22
|
+
// recorded session afterwards, while PACE agrees fresh keys every time.
|
|
23
|
+
//
|
|
24
|
+
// But this SDK's BAC has been reading real passports for a long time and its
|
|
25
|
+
// PACE has not read any, so the default here is deliberately the other way
|
|
26
|
+
// round: BAC first, PACE only when BAC is refused. In that order PACE can only
|
|
27
|
+
// ever ADD documents we can read — chips that have retired BAC — and can never
|
|
28
|
+
// take away one that already worked.
|
|
29
|
+
//
|
|
30
|
+
// PACE has since been confirmed against a real document — a Nigerian e-passport
|
|
31
|
+
// on 2026-08-11, over PACE-ECDH-GM with AES-256 on brainpoolP256r1, on both an
|
|
32
|
+
// iPhone 16 Pro Max and a Galaxy S24, with passive authentication passing and
|
|
33
|
+
// the DG2 portrait read. So the code is no longer unproven.
|
|
34
|
+
//
|
|
35
|
+
// The ordering STAYS BAC-first anyway, which is the part worth explaining:
|
|
36
|
+
// "confirmed on one document" is not "confirmed on the population". BAC has
|
|
37
|
+
// read every passport this SDK has ever seen; PACE has read one model of one
|
|
38
|
+
// issuer's. Going PACE-first would put the less-travelled path in front of
|
|
39
|
+
// every document in the world to buy a property (forward secrecy against a
|
|
40
|
+
// recorded session) that matters far less than reading the passport at all.
|
|
41
|
+
//
|
|
42
|
+
// PACE still runs — as the fallback, where it can only ever ADD documents we
|
|
43
|
+
// can read, namely chips that have retired BAC. Revisit when PACE has spanned
|
|
44
|
+
// several issuers, not before.
|
|
45
|
+
//
|
|
46
|
+
// Mirrors the Flutter SDK's emrtd_open.dart, including this reasoning.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Which access protocol is tried first.
|
|
51
|
+
*
|
|
52
|
+
* `false` (the shipping default) means BAC first, PACE only if BAC is refused.
|
|
53
|
+
* `true` reverses it, which is how PACE gets exercised against a real chip: a
|
|
54
|
+
* passport that accepts BAC would otherwise never reach the PACE code at all.
|
|
55
|
+
* Set it to `true` temporarily to test PACE against a document; do not ship it.
|
|
56
|
+
*
|
|
57
|
+
* Safe either way — whichever goes first, the other still runs as the fallback,
|
|
58
|
+
* so this cannot turn a readable document into an unreadable one.
|
|
59
|
+
*/
|
|
60
|
+
export const PREFER_PACE = false;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Why a session ended up on the protocol it did.
|
|
64
|
+
*
|
|
65
|
+
* The protocol name alone cannot answer the question that matters while PACE is
|
|
66
|
+
* new: a chip reading over BAC may never have OFFERED PACE, or may have offered
|
|
67
|
+
* it and had our implementation fail. Those call for opposite responses — one
|
|
68
|
+
* is nothing to do, the other is a bug — so the reason is recorded rather than
|
|
69
|
+
* inferred.
|
|
70
|
+
*/
|
|
71
|
+
export type PaceOutcome =
|
|
72
|
+
/** PACE opened the session. */
|
|
73
|
+
| 'used'
|
|
74
|
+
/** The chip published no EF.CardAccess: it does not speak PACE at all. */
|
|
75
|
+
| 'notOffered'
|
|
76
|
+
/** EF.CardAccess offers only variants this build cannot run. */
|
|
77
|
+
| 'unsupportedVariant'
|
|
78
|
+
/** PACE was attempted and did not complete. THIS is the one worth chasing. */
|
|
79
|
+
| 'failed'
|
|
80
|
+
/** Not tried — BAC succeeded first. */
|
|
81
|
+
| 'notAttempted';
|
|
82
|
+
|
|
83
|
+
export interface AccessResult {
|
|
84
|
+
sm: SecureMessagingSession;
|
|
85
|
+
/** Which protocol secured the session. */
|
|
86
|
+
chipAuth: 'bac' | 'pace';
|
|
87
|
+
outcome: PaceOutcome;
|
|
88
|
+
/** The negotiated protocol when PACE was used, or why it was not. */
|
|
89
|
+
detail?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** EF.CardAccess: at the Master File, readable with no session at all. */
|
|
93
|
+
const EF_CARD_ACCESS = 0x011c;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read EF.CardAccess, or null when the chip does not offer it — which simply
|
|
97
|
+
* means BAC. Absent, unreadable, and unparseable all mean the same thing to
|
|
98
|
+
* the caller, so none of them throw.
|
|
99
|
+
*/
|
|
100
|
+
export async function readCardAccess(transport: EmrtdTransport): Promise<Uint8Array | null> {
|
|
101
|
+
try {
|
|
102
|
+
// SELECT the Master File, then EF.CardAccess by identifier.
|
|
103
|
+
const mf = await transport.transceive(
|
|
104
|
+
toBase64(new Uint8Array([0x00, 0xa4, 0x00, 0x0c, 0x02, 0x3f, 0x00])),
|
|
105
|
+
);
|
|
106
|
+
if (mf.statusWord !== 0x9000) return null;
|
|
107
|
+
const select = await transport.transceive(
|
|
108
|
+
toBase64(
|
|
109
|
+
new Uint8Array([0x00, 0xa4, 0x02, 0x0c, 0x02, (EF_CARD_ACCESS >> 8) & 0xff, EF_CARD_ACCESS & 0xff]),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
if (select.statusWord !== 0x9000) return null;
|
|
113
|
+
const read = await transport.transceive(
|
|
114
|
+
toBase64(new Uint8Array([0x00, 0xb0, 0x00, 0x00, 0x00])),
|
|
115
|
+
);
|
|
116
|
+
if (read.statusWord !== 0x9000) return null;
|
|
117
|
+
const data = fromBase64(read.data);
|
|
118
|
+
return data.length > 0 ? data : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null; // absent or unreadable — the caller falls back to BAC
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Try PACE. Resolves with the session when it worked, or reports why it did
|
|
126
|
+
* not — a `null` result always means "fall back to BAC", never a failed read.
|
|
127
|
+
*/
|
|
128
|
+
export async function tryPace(
|
|
129
|
+
p: EmrtdPrimitives,
|
|
130
|
+
transport: EmrtdTransport,
|
|
131
|
+
mrz: MrzKeyFields,
|
|
132
|
+
): Promise<{ sm: SecureMessagingSession; detail: string } | { outcome: PaceOutcome; detail?: string }> {
|
|
133
|
+
const file = await readCardAccess(transport);
|
|
134
|
+
if (!file) return { outcome: 'notOffered' };
|
|
135
|
+
|
|
136
|
+
const offers = parseCardAccess(file);
|
|
137
|
+
const selected = selectPaceOffer(offers);
|
|
138
|
+
if (!selected) {
|
|
139
|
+
return { outcome: 'unsupportedVariant', detail: paceGapFor(offers) ?? undefined };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { offer, curve } = selected;
|
|
143
|
+
// The MRZ unlocks the chip's nonce rather than the session itself: counter 3
|
|
144
|
+
// is the password key, where BAC uses 1 and 2 for its session keys. The seed
|
|
145
|
+
// is the UNTRUNCATED SHA-1 — see paceKeySeed for why that distinction matters
|
|
146
|
+
// and how it fails when it is wrong.
|
|
147
|
+
const passwordKey = offer.protocol.suite.deriveKey(p, paceKeySeed(p, mrz), 3);
|
|
148
|
+
|
|
149
|
+
const transceive: PaceTransceive = async (command) => {
|
|
150
|
+
const res = await transport.transceive(toBase64(command));
|
|
151
|
+
return { data: fromBase64(res.data), statusWord: res.statusWord };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const sm = await runPaceEcdhGm({
|
|
155
|
+
p,
|
|
156
|
+
transceive,
|
|
157
|
+
protocol: offer.protocol,
|
|
158
|
+
curve,
|
|
159
|
+
passwordKey,
|
|
160
|
+
});
|
|
161
|
+
return { sm, detail: `${describeProtocol(offer.protocol)} params=${offer.parameterId}` };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Whether a parameter id resolves to a curve this build runs. Test seam. */
|
|
165
|
+
export function supportsParameterId(id: number): boolean {
|
|
166
|
+
return curveForParameterId(id) !== null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { PaceError };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { readTlv, readTlvSequence } from './der';
|
|
2
|
+
import { curveForParameterId, type EcCurve } from './ec-curves';
|
|
3
|
+
import { DES_EDE2_SUITE, aesSuite, type CipherSuite } from './suites';
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// PACE protocols, domain parameters, and EF.CardAccess (ICAO 9303 Part 11 §9.2).
|
|
7
|
+
//
|
|
8
|
+
// A chip advertises which PACE variants it supports as object identifiers in
|
|
9
|
+
// EF.CardAccess — a small file at the Master File level, readable with NO
|
|
10
|
+
// session at all, which is how a terminal discovers whether the chip speaks
|
|
11
|
+
// PACE and with which parameters.
|
|
12
|
+
//
|
|
13
|
+
// Only Generic Mapping over elliptic curves is implemented. Integrated Mapping
|
|
14
|
+
// and Chip Authentication Mapping are distinct protocols rather than
|
|
15
|
+
// variations, and PACE over finite-field Diffie-Hellman needs the RFC 5114
|
|
16
|
+
// modular groups embedded as constants, where a subtly wrong 2048-bit prime
|
|
17
|
+
// fails in a way that looks like a bad document. Virtually every issued
|
|
18
|
+
// passport offers the elliptic-curve variants, and anything reaching those gaps
|
|
19
|
+
// still reads over BAC.
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/** How the chip's nonce is mapped onto a fresh generator. */
|
|
23
|
+
export type PaceMapping = 'generic' | 'integrated' | 'chipAuthentication';
|
|
24
|
+
|
|
25
|
+
/** Which key-agreement primitive the mapping runs over. */
|
|
26
|
+
export type PaceKeyAgreement = 'dh' | 'ecdh';
|
|
27
|
+
|
|
28
|
+
/** A PACE variant the chip advertised. */
|
|
29
|
+
export interface PaceProtocol {
|
|
30
|
+
/**
|
|
31
|
+
* The raw OID bytes. Re-sent to the chip and folded into the authentication
|
|
32
|
+
* tokens, so they must be preserved exactly as received.
|
|
33
|
+
*/
|
|
34
|
+
readonly oid: Uint8Array;
|
|
35
|
+
readonly mapping: PaceMapping;
|
|
36
|
+
readonly keyAgreement: PaceKeyAgreement;
|
|
37
|
+
readonly suite: CipherSuite;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether this build can actually run the variant. */
|
|
41
|
+
export function isSupportedProtocol(protocol: PaceProtocol): boolean {
|
|
42
|
+
return protocol.mapping === 'generic' && protocol.keyAgreement === 'ecdh';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function describeProtocol(protocol: PaceProtocol): string {
|
|
46
|
+
return `PACE-${protocol.keyAgreement.toUpperCase()}-${protocol.mapping} ${protocol.suite.name}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `0.4.0.127.0.7.2.2.4` — the arc every PACE protocol identifier sits under. */
|
|
50
|
+
const PACE_ARC = [0x04, 0x00, 0x7f, 0x00, 0x07, 0x02, 0x02, 0x04];
|
|
51
|
+
|
|
52
|
+
/** The last-but-one arc component selects mapping + key agreement. */
|
|
53
|
+
const BRANCHES: Record<number, [PaceMapping, PaceKeyAgreement]> = {
|
|
54
|
+
1: ['generic', 'dh'],
|
|
55
|
+
2: ['generic', 'ecdh'],
|
|
56
|
+
3: ['integrated', 'dh'],
|
|
57
|
+
4: ['integrated', 'ecdh'],
|
|
58
|
+
6: ['chipAuthentication', 'ecdh'],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** The final component selects the cipher suite. */
|
|
62
|
+
const CIPHERS: Record<number, CipherSuite> = {
|
|
63
|
+
1: DES_EDE2_SUITE,
|
|
64
|
+
2: aesSuite(16),
|
|
65
|
+
3: aesSuite(24),
|
|
66
|
+
4: aesSuite(32),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decode a PACE protocol OID, or null when it is not one.
|
|
71
|
+
*
|
|
72
|
+
* An unknown OID in EF.CardAccess is NORMAL, not an error: the file also
|
|
73
|
+
* carries security information for chip authentication and terminal
|
|
74
|
+
* authentication, which this SDK does not run.
|
|
75
|
+
*/
|
|
76
|
+
export function paceProtocolFromOid(oid: Uint8Array): PaceProtocol | null {
|
|
77
|
+
if (oid.length !== PACE_ARC.length + 2) return null;
|
|
78
|
+
for (let i = 0; i < PACE_ARC.length; i++) {
|
|
79
|
+
if (oid[i] !== PACE_ARC[i]) return null;
|
|
80
|
+
}
|
|
81
|
+
const branch = BRANCHES[oid[PACE_ARC.length]!];
|
|
82
|
+
const suite = CIPHERS[oid[PACE_ARC.length + 1]!];
|
|
83
|
+
if (!branch || !suite) return null;
|
|
84
|
+
return { oid: new Uint8Array(oid), mapping: branch[0], keyAgreement: branch[1], suite };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One PACE offering from the chip. */
|
|
88
|
+
export interface PaceOffer {
|
|
89
|
+
readonly protocol: PaceProtocol;
|
|
90
|
+
/** The standardised domain-parameter id, naming a curve or DH group. */
|
|
91
|
+
readonly parameterId: number | null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The PACE offerings advertised in EF.CardAccess, in file order.
|
|
96
|
+
*
|
|
97
|
+
* Returns empty for a chip that advertises no PACE, for a file that cannot be
|
|
98
|
+
* parsed, and for one that is simply absent — all of which mean the same thing
|
|
99
|
+
* to the caller: use BAC.
|
|
100
|
+
*/
|
|
101
|
+
export function parseCardAccess(file: Uint8Array): PaceOffer[] {
|
|
102
|
+
const offers: PaceOffer[] = [];
|
|
103
|
+
// The file is a SET (0x31) of SEQUENCEs. Some chips wrap it differently, so
|
|
104
|
+
// accept the SET's contents or a bare run of SEQUENCEs.
|
|
105
|
+
const outer = readTlv(file);
|
|
106
|
+
const body = outer && outer.tag === 0x31 ? outer.value : file;
|
|
107
|
+
|
|
108
|
+
for (const info of readTlvSequence(body)) {
|
|
109
|
+
if (info.tag !== 0x30) continue; // not a SecurityInfo
|
|
110
|
+
const fields = readTlvSequence(info.value);
|
|
111
|
+
const first = fields[0];
|
|
112
|
+
if (!first || first.tag !== 0x06) continue;
|
|
113
|
+
|
|
114
|
+
const protocol = paceProtocolFromOid(first.value);
|
|
115
|
+
if (!protocol) continue; // some other protocol's security info
|
|
116
|
+
|
|
117
|
+
// PACEInfo ::= SEQUENCE { protocol OID, version INTEGER,
|
|
118
|
+
// parameterId INTEGER OPTIONAL }
|
|
119
|
+
const ints = fields.slice(1).filter((f) => f.tag === 0x02);
|
|
120
|
+
const parameterId = ints.length > 1 ? intFromBytes(ints[1]!.value) : null;
|
|
121
|
+
offers.push({ protocol, parameterId });
|
|
122
|
+
}
|
|
123
|
+
return offers;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A small DER INTEGER as a number. */
|
|
127
|
+
function intFromBytes(bytes: Uint8Array): number {
|
|
128
|
+
let n = 0;
|
|
129
|
+
for (const b of bytes) n = n * 256 + b;
|
|
130
|
+
return n;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** A usable offering plus its resolved curve. */
|
|
134
|
+
export interface SelectedPaceOffer {
|
|
135
|
+
readonly offer: PaceOffer;
|
|
136
|
+
readonly curve: EcCurve;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The offering to actually attempt, or null when none is usable.
|
|
141
|
+
*
|
|
142
|
+
* Prefers the strongest cipher the chip offers among the variants this build
|
|
143
|
+
* supports, so a chip advertising both AES-256 and 3DES is run at AES-256.
|
|
144
|
+
*/
|
|
145
|
+
export function selectPaceOffer(offers: PaceOffer[]): SelectedPaceOffer | null {
|
|
146
|
+
let best: SelectedPaceOffer | null = null;
|
|
147
|
+
for (const offer of offers) {
|
|
148
|
+
if (!isSupportedProtocol(offer.protocol)) continue;
|
|
149
|
+
if (offer.parameterId === null) continue;
|
|
150
|
+
const curve = curveForParameterId(offer.parameterId);
|
|
151
|
+
if (!curve) continue;
|
|
152
|
+
if (!best || offer.protocol.suite.keyLength > best.offer.protocol.suite.keyLength) {
|
|
153
|
+
best = { offer, curve };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return best;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Why no offering was usable. Diagnostics only — the read falls back to BAC. */
|
|
160
|
+
export type PaceGap = 'mapping' | 'keyAgreement' | 'domainParameters';
|
|
161
|
+
|
|
162
|
+
export function paceGapFor(offers: PaceOffer[]): PaceGap | null {
|
|
163
|
+
if (offers.length === 0) return null;
|
|
164
|
+
for (const offer of offers) {
|
|
165
|
+
if (offer.protocol.keyAgreement !== 'ecdh') return 'keyAgreement';
|
|
166
|
+
if (offer.protocol.mapping !== 'generic') return 'mapping';
|
|
167
|
+
}
|
|
168
|
+
return 'domainParameters';
|
|
169
|
+
}
|