import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import { PassportChallenge } from '../ts/reception/classes.passportchallenge.js'; import { PassportDevice } from '../ts/reception/classes.passportdevice.js'; import { PassportManager } from '../ts/reception/classes.passportmanager.js'; import { PassportNonce } from '../ts/reception/classes.passportnonce.js'; const getNestedValue = (targetArg: any, pathArg: string) => { return pathArg.split('.').reduce((currentArg, keyArg) => currentArg?.[keyArg], targetArg); }; const matchesQuery = (targetArg: any, queryArg: Record) => { return Object.entries(queryArg).every(([keyArg, valueArg]) => { return getNestedValue(targetArg, keyArg) === valueArg; }); }; const createTestPassportManager = () => { const passportDevices = new Map(); const passportChallenges = new Map(); const passportNonces = new Map(); const activityLogCalls: Array<{ userId: string; action: string; description: string; }> = []; const deliveredHintIds: string[] = []; const manager = new PassportManager({ db: { smartdataDb: {} }, typedrouter: { addTypedRouter: () => undefined }, options: { baseUrl: 'https://idp.global' }, jwtManager: { verifyJWTAndGetData: async () => null }, activityLogManager: { logActivity: async (userIdArg: string, actionArg: string, descriptionArg: string) => { activityLogCalls.push({ userId: userIdArg, action: actionArg, description: descriptionArg, }); }, }, passportPushManager: { deliverChallengeHint: async (_passportDeviceArg: PassportDevice, passportChallengeArg: PassportChallenge) => { deliveredHintIds.push(passportChallengeArg.data.notification!.hintId); passportChallengeArg.data.notification = { ...passportChallengeArg.data.notification!, status: 'sent', attemptCount: passportChallengeArg.data.notification!.attemptCount + 1, deliveredAt: Date.now(), lastError: null, }; await passportChallengeArg.save(); return true; }, }, } as any); const originalPassportDeviceSave = PassportDevice.prototype.save; const originalPassportDeviceDelete = PassportDevice.prototype.delete; const originalPassportChallengeSave = PassportChallenge.prototype.save; const originalPassportChallengeDelete = PassportChallenge.prototype.delete; const originalPassportNonceSave = PassportNonce.prototype.save; const originalPassportNonceDelete = PassportNonce.prototype.delete; (PassportDevice.prototype as PassportDevice & { save: () => Promise }).save = async function () { passportDevices.set(this.id, this); }; (PassportDevice.prototype as PassportDevice & { delete: () => Promise }).delete = async function () { passportDevices.delete(this.id); }; (PassportChallenge.prototype as PassportChallenge & { save: () => Promise }).save = async function () { passportChallenges.set(this.id, this); }; (PassportChallenge.prototype as PassportChallenge & { delete: () => Promise }).delete = async function () { passportChallenges.delete(this.id); }; (PassportNonce.prototype as PassportNonce & { save: () => Promise }).save = async function () { passportNonces.set(this.id, this); }; (PassportNonce.prototype as PassportNonce & { delete: () => Promise }).delete = async function () { passportNonces.delete(this.id); }; (manager as any).CPassportDevice = { getInstance: async (queryArg: Record) => { return Array.from(passportDevices.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null; }, getInstances: async (queryArg: Record) => { return Array.from(passportDevices.values()).filter((docArg) => matchesQuery(docArg, queryArg)); }, }; (manager as any).CPassportChallenge = { getInstance: async (queryArg: Record) => { return ( Array.from(passportChallenges.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null ); }, getInstances: async (queryArg: Record) => { return Array.from(passportChallenges.values()).filter((docArg) => matchesQuery(docArg, queryArg)); }, }; (manager as any).CPassportNonce = { getInstance: async (queryArg: Record) => { return Array.from(passportNonces.values()).find((docArg) => matchesQuery(docArg, queryArg)) || null; }, getInstances: async (queryArg: Record) => { return Array.from(passportNonces.values()).filter((docArg) => matchesQuery(docArg, queryArg)); }, }; return { manager, passportDevices, passportChallenges, passportNonces, activityLogCalls, deliveredHintIds, restore: () => { PassportDevice.prototype.save = originalPassportDeviceSave; PassportDevice.prototype.delete = originalPassportDeviceDelete; PassportChallenge.prototype.save = originalPassportChallengeSave; PassportChallenge.prototype.delete = originalPassportChallengeDelete; PassportNonce.prototype.save = originalPassportNonceSave; PassportNonce.prototype.delete = originalPassportNonceDelete; }, }; }; const createRawPassportSigner = async () => { const subtle = plugins.crypto.webcrypto.subtle; const keyPair = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [ 'sign', 'verify', ]); const publicKeyRaw = Buffer.from(await subtle.exportKey('raw', keyPair.publicKey)).toString('base64'); return { publicKeyX963Base64: publicKeyRaw, sign: async (payloadArg: string) => { const signature = await subtle.sign( { name: 'ECDSA', hash: 'SHA-256' }, keyPair.privateKey, Buffer.from(payloadArg, 'utf8') ); return Buffer.from(signature).toString('base64'); }, }; }; const createDerPassportSigner = () => { const keyPair = plugins.crypto.generateKeyPairSync('ec', { namedCurve: 'prime256v1' }); const publicJwk = keyPair.publicKey.export({ format: 'jwk' }) as JsonWebKey; const publicKeyX963Base64 = Buffer.concat([ Buffer.from([4]), Buffer.from(publicJwk.x!, 'base64url'), Buffer.from(publicJwk.y!, 'base64url'), ]).toString('base64'); return { publicKeyX963Base64, sign: (payloadArg: string) => { return plugins.crypto.sign('sha256', Buffer.from(payloadArg, 'utf8'), keyPair.privateKey).toString('base64'); }, }; }; const createSignedDeviceRequest = async ( managerArg: PassportManager, signerArg: { sign: (payloadArg: string) => Promise | string }, requestArg: { deviceId: string; action: string; signedFields?: string[]; } ) => { const baseRequest = { deviceId: requestArg.deviceId, timestamp: Date.now(), nonce: plugins.crypto.randomUUID(), }; const payload = (managerArg as any).buildDeviceRequestSigningPayload( baseRequest, requestArg.action, requestArg.signedFields || [] ); return { ...baseRequest, signatureBase64: await signerArg.sign(payload), signatureFormat: 'raw' as const, }; }; tap.test('enrolls a passport device from a pairing challenge', async () => { const { manager, passportDevices, passportChallenges, activityLogCalls, restore } = createTestPassportManager(); try { const enrollment = await manager.createEnrollmentChallengeForUser('user-1', { deviceLabel: 'Phil iPhone', platform: 'ios', capabilities: { gps: true, nfc: true, push: true, }, }); const signer = await createRawPassportSigner(); const signatureBase64 = await signer.sign(enrollment.signingPayload); const passportDevice = await manager.completeEnrollment({ pairingToken: enrollment.pairingToken, deviceLabel: 'Phil iPhone', platform: 'ios', publicKeyX963Base64: signer.publicKeyX963Base64, signatureBase64, signatureFormat: 'raw', capabilities: { gps: true, nfc: true, push: true, }, appVersion: '1.0.0', }); expect(passportDevice.data.userId).toEqual('user-1'); expect(passportDevice.data.label).toEqual('Phil iPhone'); expect(passportDevices.size).toEqual(1); expect(passportChallenges.size).toEqual(1); expect(Array.from(passportChallenges.values())[0].data.status).toEqual('approved'); expect(activityLogCalls[0].action).toEqual('passport_device_enrolled'); } finally { restore(); } }); tap.test('creates and approves a passport challenge with DER signatures and evidence', async () => { const { manager, activityLogCalls, deliveredHintIds, restore } = createTestPassportManager(); try { const enrollment = await manager.createEnrollmentChallengeForUser('user-2', { deviceLabel: 'Office iPhone', platform: 'ios', capabilities: { gps: true, nfc: true, push: true, }, }); const signer = createDerPassportSigner(); const passportDevice = await manager.completeEnrollment({ pairingToken: enrollment.pairingToken, deviceLabel: 'Office iPhone', platform: 'ios', publicKeyX963Base64: signer.publicKeyX963Base64, signatureBase64: signer.sign(enrollment.signingPayload), signatureFormat: 'der', capabilities: { gps: true, nfc: true, push: true, }, }); const challengeResult = await manager.createPassportChallengeForUser('user-2', { type: 'physical_access', preferredDeviceId: passportDevice.id, audience: 'hq-door-a', notificationTitle: 'Office entry request', requireLocation: true, requireNfc: true, locationPolicy: { mode: 'geofence', label: 'HQ Berlin', latitude: 53.0793, longitude: 8.8017, radiusMeters: 80, maxAccuracyMeters: 25, }, }); expect(deliveredHintIds).toHaveLength(1); expect(challengeResult.challenge.data.notification?.status).toEqual('sent'); await expect( manager.approvePassportChallenge({ challengeId: challengeResult.challenge.id, deviceId: passportDevice.id, signatureBase64: signer.sign(challengeResult.signingPayload), signatureFormat: 'der', location: { latitude: 53.5, longitude: 8.1, accuracyMeters: 12, capturedAt: Date.now(), }, nfc: { readerId: 'door-reader-a', }, }) ).rejects.toThrow(); const approvedChallenge = await manager.approvePassportChallenge({ challengeId: challengeResult.challenge.id, deviceId: passportDevice.id, signatureBase64: signer.sign(challengeResult.signingPayload), signatureFormat: 'der', location: { latitude: 53.0793, longitude: 8.8017, accuracyMeters: 12, capturedAt: Date.now(), }, nfc: { readerId: 'door-reader-a', }, }); expect(approvedChallenge.data.status).toEqual('approved'); expect(approvedChallenge.data.evidence?.signatureFormat).toEqual('der'); expect(approvedChallenge.data.evidence?.location?.accuracyMeters).toEqual(12); expect(approvedChallenge.data.evidence?.locationEvaluation?.matched).toBeTrue(); expect(approvedChallenge.data.evidence?.nfc?.readerId).toEqual('door-reader-a'); expect(activityLogCalls.at(-1)?.action).toEqual('passport_challenge_approved'); } finally { restore(); } }); tap.test('registers push tokens and loads pending challenges through signed device requests', async () => { const { manager, passportNonces, restore } = createTestPassportManager(); try { const enrollment = await manager.createEnrollmentChallengeForUser('user-3', { deviceLabel: 'Work iPhone', platform: 'ios', capabilities: { gps: true, nfc: false, push: true, }, }); const signer = await createRawPassportSigner(); const passportDevice = await manager.completeEnrollment({ pairingToken: enrollment.pairingToken, deviceLabel: 'Work iPhone', platform: 'ios', publicKeyX963Base64: signer.publicKeyX963Base64, signatureBase64: await signer.sign(enrollment.signingPayload), signatureFormat: 'raw', capabilities: { gps: true, nfc: false, push: true, }, }); const pushRequest = await createSignedDeviceRequest(manager, signer, { deviceId: passportDevice.id, action: 'registerPassportPushToken', signedFields: [ 'provider=apns', 'token=device-token-1', 'topic=global.idp.swiftapp', 'environment=development', ], }); const registeredPassportDevice = await (manager as any).authenticatePassportDeviceRequest( { ...pushRequest, }, { action: 'registerPassportPushToken', signedFields: [ 'provider=apns', 'token=device-token-1', 'topic=global.idp.swiftapp', 'environment=development', ], } ); registeredPassportDevice.data.pushRegistration = { provider: 'apns', token: 'device-token-1', topic: 'global.idp.swiftapp', environment: 'development', registeredAt: Date.now(), }; await registeredPassportDevice.save(); const challengeResult = await manager.createPassportChallengeForUser('user-3', { type: 'authentication', preferredDeviceId: passportDevice.id, audience: 'office-saas', notificationTitle: 'Office sign-in verification', }); const listRequest = await createSignedDeviceRequest(manager, signer, { deviceId: passportDevice.id, action: 'listPendingPassportChallenges', }); const authenticatedDevice = await (manager as any).authenticatePassportDeviceRequest(listRequest, { action: 'listPendingPassportChallenges', }); const pendingChallenges = await manager.listPendingChallengesForDevice(authenticatedDevice.id); expect(pendingChallenges).toHaveLength(1); expect(pendingChallenges[0].id).toEqual(challengeResult.challenge.id); const hintId = challengeResult.challenge.data.notification!.hintId; const getRequest = await createSignedDeviceRequest(manager, signer, { deviceId: passportDevice.id, action: 'getPassportChallengeByHint', signedFields: [`hint_id=${hintId}`], }); const hintChallenge = await manager.getPassportChallengeByHint( ( await (manager as any).authenticatePassportDeviceRequest(getRequest, { action: 'getPassportChallengeByHint', signedFields: [`hint_id=${hintId}`], }) ).id, hintId ); expect(hintChallenge?.id).toEqual(challengeResult.challenge.id); const seenRequest = await createSignedDeviceRequest(manager, signer, { deviceId: passportDevice.id, action: 'markPassportChallengeSeen', signedFields: [`hint_id=${hintId}`], }); await (manager as any).authenticatePassportDeviceRequest(seenRequest, { action: 'markPassportChallengeSeen', signedFields: [`hint_id=${hintId}`], }); const seenChallenge = await manager.markPassportChallengeSeen(passportDevice.id, hintId); expect(seenChallenge.data.notification?.status).toEqual('seen'); expect(passportNonces.size).toEqual(4); } finally { restore(); } }); export default tap.start();