diff options
Diffstat (limited to 'packages/server/src')
5 files changed, 62 insertions, 131 deletions
diff --git a/packages/server/src/extensions/devicePubKey.test.ts b/packages/server/src/extensions/devicePubKey.test.ts deleted file mode 100644 index 10051c7..0000000 --- a/packages/server/src/extensions/devicePubKey.test.ts +++ /dev/null @@ -1 +0,0 @@ -// Placeholder diff --git a/packages/server/src/extensions/devicePubKey.ts b/packages/server/src/extensions/devicePubKey.ts deleted file mode 100644 index 163616b..0000000 --- a/packages/server/src/extensions/devicePubKey.ts +++ /dev/null @@ -1,120 +0,0 @@ -import cbor from 'cbor'; -import base64url from 'base64url'; -import { AttestationFormat, AttestationStatement } from '../helpers/decodeAttestationObject'; -import { RegistrationCredentialJSON } from '@simplewebauthn/typescript-types'; -import { CredentialPropertiesOutput, UvmEntries } from '@simplewebauthn/typescript-types'; -import { parseAuthenticatorData, verifySignature, decodeCredentialPublicKey } from 'helpers'; -import { COSEKEYS } from '../helpers/convertCOSEtoPKCS'; - -export function decodeAttObjForDevicePublicKey(attObjForDevicePublicKey: Buffer): AttObjForDevicePublicKey { - const toCBOR: AttObjForDevicePublicKey = cbor.decodeAllSync(attObjForDevicePublicKey)[0]; - return toCBOR; -} - -export async function verifyAttObjForDevicePublicKey( - credential: RegistrationCredentialJSON, - attObjForDevicePublicKey: AttObjForDevicePublicKey, - authData: Buffer, - hash: Buffer -): Promise<boolean> { - const { credentialID, credentialPublicKey } = parseAuthenticatorData(authData); - if (!credentialID) { - throw new Error('No credential ID was provided by authenticator'); - } - if (!credentialPublicKey) { - throw new Error('No credential public key was provided by authenticator'); - } - const decodedPublicKey = decodeCredentialPublicKey(credentialPublicKey); - const alg = decodedPublicKey.get(COSEKEYS.alg); - - if (typeof alg !== 'number') { - throw new Error('Credential public key was missing numeric alg'); - } - - // Make sure the key algorithm is one we specified within the registration options - if (!supportedAlgorithmIDs.includes(alg as number)) { - const supported = supportedAlgorithmIDs.join(', '); - throw new Error(`Unexpected public key alg "${alg}", expected one of "${supported}"`); - } - - const rootCertificates = settingsService.getRootCertificates({ identifier: fmt }); - - const { sig, aaguid, dpk, nonce, fmt, attStmt } = attObjForDevicePublicKey; - - // Verify that `sig` is a valid signature over the concatenation of `hash` and - // `credentialId` using the device public key `dpk` (the signature algorithm - // is indicated by dpk’s "alg" COSEAlgorithmIdentifier value). - const signatureBase = Buffer.concat([hash, credentialID]); - verifySignature(sig, signatureBase, dpk); - - // Verify that `attStmt` is a correct attestation statement, conveying a valid - // attestation signature, by using the attestation statement format `fmt`’s - // verification procedure given `attStmt`, although substituting `aaguid`’s - // value for `authenticatorData`, and substituting the concatenation of - // `dpk`’s value and `nonce`’s value for `clientDataHash` in the attestation - // statement format's verification procedure inputs. - // Note: If `fmt’`s value is "none" there is no attestation signature to - // verify. - const clientDataHash = Buffer.concat([dpk, nonce]); - - // Prepare arguments to pass to the relevant verification method - const verifierOpts: AttestationFormatVerifierOpts = { - aaguid, - attStmt, - authData, - clientDataHash, - credentialID, - credentialPublicKey, - rootCertificates, - rpIdHash, - }; - - /** - * Verification can only be performed when attestation = 'direct' - */ - let verified = false; - if (fmt === 'fido-u2f') { - verified = await verifyFIDOU2F(verifierOpts); - } else if (fmt === 'packed') { - verified = await verifyPacked(verifierOpts); - } else if (fmt === 'android-safetynet') { - verified = await verifyAndroidSafetynet(verifierOpts); - } else if (fmt === 'android-key') { - verified = await verifyAndroidKey(verifierOpts); - } else if (fmt === 'tpm') { - verified = await verifyTPM(verifierOpts); - } else if (fmt === 'apple') { - verified = await verifyApple(verifierOpts); - } else if (fmt === 'none') { - if (Object.keys(attStmt).length > 0) { - throw new Error('None attestation had unexpected attestation statement'); - } - // This is the weaker of the attestations, so there's nothing else to really check - verified = true; - } else { - throw new Error(`Unsupported Attestation Format: ${fmt}`); - } - - - // Return the `aaguid`, `dpk`, `scope`, `fmt`, `attStmt` values indexed to the - // `credential.id`. - - return true; -} - -export type AttObjForDevicePublicKey = { - sig: Buffer; - aaguid: Buffer; - dpk: Buffer; - scope: number; - nonce: Buffer; - fmt: AttestationFormat; - attStmt: AttestationStatement; -}; - -export type AuthenticationExtensionsClientOutputs = { - appid?: boolean; - credProps?: CredentialPropertiesOutput; - uvm?: UvmEntries; - devicePubKey?: AttObjForDevicePublicKey; -}; diff --git a/packages/server/src/helpers/decodeExtensions.ts b/packages/server/src/helpers/decodeExtensions.ts new file mode 100644 index 0000000..f1b5cb3 --- /dev/null +++ b/packages/server/src/helpers/decodeExtensions.ts @@ -0,0 +1,18 @@ +import cbor from 'cbor'; + +/** + * Convert an extension data buffer to a proper object + * + * @param extensionDataBuffer Extension Data buffer + */ +export default function decodeExtensionDataBuffer(extensionDataBuffer: Buffer): ExtensionsJSON { + const toCBOR: ExtensionsJSON = cbor.decodeAllSync(extensionDataBuffer)[0]; + return toCBOR; +} + +export type ExtensionsJSON = { + dpk?: Buffer; + scp?: Buffer; + sig?: string; + aaguid?: Buffer; +}; diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 03f74ef..931ece1 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -580,6 +580,20 @@ test('should return credential backup info', async () => { expect(verification.registrationInfo?.credentialBackedUp).toEqual(false); }); +test('should return extension', async () => { + const verification = await verifyRegistrationResponse({ + credential: attestationDPK, + expectedChallenge: attestationDPKChallenge, + expectedOrigin: 'android:apk-key-hash:gx7sq_pxhxhrIQdLyfG0pxKwiJ7hOk2DJQ4xvKd438Q', + expectedRPID: 'try-webauthn.appspot.com', + }); + + expect(verification.registrationInfo?.extensions?.dpk).toEqual(Buffer.from('3059301306072A8648CE3D020106082A8648CE3D030107034200046F985BC21D0AD79C63F2430FB52E8905585AE9372AE250B10491FED48822D150AAD22215E511B73C39835FF0F89D7BB4E910E18DDB5DB968CD13890F348DE867', 'hex')); + expect(verification.registrationInfo?.extensions?.scp).toEqual('device'); + expect(verification.registrationInfo?.extensions?.sig).toEqual(Buffer.from('3046022100E28B11DB74A4450336320119A4E8E14624C4E715A22E29DBAFC6AA3A383FD63E022100CF41F069CC511AE77A7288F703C8E0F67255DEFCB5A56C83B16E35E0F38B2849', 'hex')); + expect(verification.registrationInfo?.extensions?.aaguid).toEqual(Buffer.from('00000000000000000000000000000000', 'hex')); +}); + /** * Various Attestations Below */ @@ -668,3 +682,20 @@ const attestationNone: RegistrationCredentialJSON = { type: 'public-key', }; const attestationNoneChallenge = base64url.encode('hEccPWuziP00H0p5gxh2_u5_PC4NeYgd'); + +const attestationDPK: RegistrationCredentialJSON = { + id: 'LcGIzt53ej1NhnMiwcmv5Q', + rawId: 'LcGIzt53ej1NhnMiwcmv5Q', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBZw11_MVj_ad52y40PupImIh1i3hUnUk6T9vqHNlqoxzExQAAAAAAAAAAAAAAAAAAAAAAAAAAABAtwYjO3nd6PU2GcyLBya_lpQECAyYgASFYIImWMQLU6wTo6sUQJLmAznqm-88GRLg1GSvr6HE9Szm4IlggrKYySExPTjIeD2o62JB3H4fyJD1TSBzwNcRfEZuwD9akY2Rwa1hbMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEb5hbwh0K15xj8kMPtS6JBVha6Tcq4lCxBJH-1Igi0VCq0iIV5RG3PDmDX_D4nXu06RDhjdtduWjNE4kPNI3oZ2NzY3BmZGV2aWNlY3NpZ1hIMEYCIQDiixHbdKRFAzYyARmk6OFGJMTnFaIuKduvxqo6OD_WPgIhAM9B8GnMURrnenKI9wPI4PZyVd78taVsg7FuNeDziyhJZmFhZ3VpZFAAAAAAAAAAAAAAAAAAAAAA', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoid2cyZGdqcnI1aWxjWW1mQ2UtY05VWllvWWlH' + + 'SVo1dDdIeFk0eV94NG9lOCIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOmd4N3NxX3B4aHhocklRZEx5' + + 'ZkcwcHhLd2lKN2hPazJESlE0eHZLZDQzOFEiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZmlkby5leGFtcGxl' + + 'LmZpZG8yYXBpZXhhbXBsZSJ9', + }, + clientExtensionResults: {}, + type: 'public-key', +}; +const attestationDPKChallenge = 'wg2dgjrr5ilcYmfCe-cNUZYoYiGIZ5t7HxY4y_x4oe8'; diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index 2cd4a86..899b624 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -9,7 +9,7 @@ import decodeAttestationObject, { AttestationFormat, AttestationStatement, } from '../helpers/decodeAttestationObject'; -// import { decodeAttObjForDevicePublicKey, verifyAttObjForDevicePublicKey } from '../extensions/devicePubKey'; +import decodeExtensionDataBuffer, { ExtensionsJSON } from '../helpers/decodeExtensions'; import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import toHash from '../helpers/toHash'; @@ -135,6 +135,15 @@ export default async function verifyRegistrationResponse( const parsedAuthData = parseAuthenticatorData(authData); const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey, extensionsDataBuffer } = parsedAuthData; + let extensions: ExtensionsJSON = {}; + + // Temporarily assume that the extension is a DPK + // TODO: This needs to be keyed object: { devicePubKey: DPK } + if (flags.ed && extensionsDataBuffer) { + const devicePublicKey = decodeExtensionDataBuffer(extensionsDataBuffer); + extensions = devicePublicKey; + } + // Make sure the response's RP ID is ours if (expectedRPID) { if (typeof expectedRPID === 'string') { @@ -193,15 +202,6 @@ export default async function verifyRegistrationResponse( const clientDataHash = toHash(base64url.toBuffer(response.clientDataJSON)); const rootCertificates = settingsService.getRootCertificates({ identifier: fmt }); - // if (clientExtensionResults) { - // if (clientExtensionResults.devicePubKey) { - // const attObjForDevicePublicKey = decodeAttObjForDevicePublicKey(clientExtensionResults.devicePubKey); - // if (!verifyAttObjForDevicePublicKey(credential, attObjForDevicePublicKey, authData, clientDataHash)) { - // throw new Error('Invalid attestation object for device public key'); - // } - // } - // } - // Prepare arguments to pass to the relevant verification method const verifierOpts: AttestationFormatVerifierOpts = { aaguid, @@ -258,6 +258,7 @@ export default async function verifyRegistrationResponse( userVerified: flags.uv, credentialDeviceType, credentialBackedUp, + extensions, }; } @@ -284,6 +285,7 @@ export default async function verifyRegistrationResponse( * @param registrationInfo.credentialBackedUp Whether or not the multi-device credential has been * backed up. Always `false` for single-device credentials. **Should be kept in a DB for later * reference!** + * @param registrationInfo?.extensions The extensions returned by the browser */ export type VerifiedRegistrationResponse = { verified: boolean; @@ -298,6 +300,7 @@ export type VerifiedRegistrationResponse = { userVerified: boolean; credentialDeviceType: CredentialDeviceType; credentialBackedUp: boolean; + extensions?: ExtensionsJSON; }; }; |