168 lines
6.2 KiB
Dart
168 lines
6.2 KiB
Dart
// FIDO2 Dart unit tests.
|
|
//
|
|
// Tests 1-2 run without any external dependency.
|
|
// Tests 3-6 require card_emulator_bridge.py on TCP port 8772:
|
|
//
|
|
// uv run --python 3.12 --with fido2 --with cbor2 --with cryptography \
|
|
// tests/card_emulator_bridge.py
|
|
//
|
|
// Then run: flutter test test/fido2_test.dart
|
|
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:cbor/cbor.dart';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../lib/ctaphid_channel.dart';
|
|
import '../lib/fido2_ops.dart';
|
|
|
|
void main() {
|
|
// -------------------------------------------------------------------------
|
|
// Test 1: CBOR encode/decode round-trip
|
|
// -------------------------------------------------------------------------
|
|
test('CBOR makeCredential request map encodes and decodes correctly', () {
|
|
final clientDataHash = Uint8List(32)..fillRange(0, 32, 0xAB);
|
|
|
|
final requestMap = CborMap({
|
|
CborSmallInt(1): CborBytes(clientDataHash),
|
|
CborSmallInt(2): CborMap({
|
|
CborString('id'): CborString(kRpId),
|
|
CborString('name'): CborString(kRpName),
|
|
}),
|
|
CborSmallInt(4): CborList([
|
|
CborMap({
|
|
CborString('type'): CborString('public-key'),
|
|
CborString('alg'): CborSmallInt(-7),
|
|
}),
|
|
]),
|
|
CborSmallInt(7): CborMap({
|
|
CborString('rk'): CborBool(false),
|
|
CborString('uv'): CborBool(false),
|
|
}),
|
|
});
|
|
|
|
final encoded = Uint8List.fromList(cbor.encode(requestMap));
|
|
expect(encoded, isNotEmpty);
|
|
|
|
final decoded = cbor.decode(encoded);
|
|
expect(decoded, isA<CborMap>());
|
|
final m = decoded as CborMap;
|
|
final hash = m[CborSmallInt(1)];
|
|
expect(hash, isA<CborBytes>());
|
|
expect((hash as CborBytes).bytes, equals(clientDataHash));
|
|
|
|
final rp = m[CborSmallInt(2)] as CborMap;
|
|
expect((rp[CborString('id')] as CborString).toString(), equals(kRpId));
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Test 2: clientDataHash is SHA256 of known JSON
|
|
// -------------------------------------------------------------------------
|
|
test('clientDataHash matches SHA256 of known clientDataJSON', () {
|
|
// Fixed challenge for deterministic test
|
|
final challenge = Uint8List.fromList(List.generate(32, (i) => i));
|
|
final challengeB64 = base64Url.encode(challenge).replaceAll('=', '');
|
|
final clientDataJson =
|
|
'{"type":"webauthn.create","challenge":"$challengeB64","origin":"$kOrigin","crossOrigin":false}';
|
|
|
|
final expected = Uint8List.fromList(sha256.convert(utf8.encode(clientDataJson)).bytes);
|
|
expect(expected.length, equals(32));
|
|
expect(expected, isNot(equals(Uint8List(32))));
|
|
});
|
|
|
|
// -------------------------------------------------------------------------
|
|
// Tests 3-6: require card_emulator_bridge.py on 127.0.0.1:8772
|
|
// -------------------------------------------------------------------------
|
|
group('emulator bridge', () {
|
|
late int cid;
|
|
late String credentialDataB64;
|
|
|
|
setUpAll(() async {
|
|
useEmulator(host: '127.0.0.1', port: 8772);
|
|
final connected = await openCard();
|
|
if (!connected) {
|
|
markTestSkipped('card_emulator_bridge.py not reachable on :8772');
|
|
return;
|
|
}
|
|
cid = await ctaphidInit();
|
|
});
|
|
|
|
tearDownAll(() async {
|
|
await closeCard();
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 3: makeCredential returns valid AttestedCredentialData
|
|
// -----------------------------------------------------------------------
|
|
test('makeCredential returns non-empty credentialData with valid structure', () async {
|
|
final result = await makeCredential(cid, 'testuser', displayName: 'Test User');
|
|
|
|
expect(result.credentialData, isNotEmpty);
|
|
expect(result.userId.length, equals(32));
|
|
|
|
// AttestedCredentialData: aaguid(16) + credIdLen(2) + credId + coseKey
|
|
final cd = result.credentialData;
|
|
expect(cd.length, greaterThan(18));
|
|
final credIdLen = (cd[16] << 8) | cd[17];
|
|
expect(credIdLen, greaterThan(0));
|
|
expect(cd.length, greaterThanOrEqualTo(18 + credIdLen + 1)); // at least 1 byte of COSE key
|
|
|
|
credentialDataB64 = result.credentialDataB64;
|
|
expect(credentialDataB64, isNotEmpty);
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 4: getAssertion returns non-empty authData and signature
|
|
// -----------------------------------------------------------------------
|
|
test('getAssertion returns authData and signature bytes', () async {
|
|
expect(credentialDataB64, isNotEmpty, reason: 'requires test 3 to pass first');
|
|
|
|
final result = await getAssertion(cid, credentialDataB64);
|
|
|
|
expect(result.authData, isNotEmpty);
|
|
expect(result.signature, isNotEmpty);
|
|
expect(result.clientDataHash.length, equals(32));
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 5: verifyAssertion accepts valid signature
|
|
// -----------------------------------------------------------------------
|
|
test('verifyAssertion accepts valid assertion signature', () async {
|
|
expect(credentialDataB64, isNotEmpty, reason: 'requires test 3 to pass first');
|
|
|
|
final assertion = await getAssertion(cid, credentialDataB64);
|
|
|
|
final ok = verifyAssertion(
|
|
credentialDataB64,
|
|
assertion.authData,
|
|
assertion.signature,
|
|
assertion.clientDataHash,
|
|
);
|
|
expect(ok, isTrue);
|
|
});
|
|
|
|
// -----------------------------------------------------------------------
|
|
// Test 6: verifyAssertion rejects tampered authData
|
|
// -----------------------------------------------------------------------
|
|
test('verifyAssertion rejects tampered authData', () async {
|
|
expect(credentialDataB64, isNotEmpty, reason: 'requires test 3 to pass first');
|
|
|
|
final assertion = await getAssertion(cid, credentialDataB64);
|
|
|
|
// Flip one byte in authData (byte 32 = flags byte)
|
|
final tampered = Uint8List.fromList(assertion.authData);
|
|
tampered[32] ^= 0xFF;
|
|
|
|
final ok = verifyAssertion(
|
|
credentialDataB64,
|
|
tampered,
|
|
assertion.signature,
|
|
assertion.clientDataHash,
|
|
);
|
|
expect(ok, isFalse);
|
|
});
|
|
});
|
|
}
|