Skip to content

Commit

Permalink
debug
Browse files Browse the repository at this point in the history
  • Loading branch information
Frederik Rothenberger committed Mar 19, 2024
1 parent 27ecd0d commit 0243040
Showing 1 changed file with 253 additions and 0 deletions.
253 changes: 253 additions & 0 deletions src/frontend/src/utils/WebAuthnIdentityCopy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
import {
DerEncodedPublicKey,
PublicKey,
Signature,
SignIdentity,
wrapDER,
DER_COSE_OID,
fromHex,
toHex,
} from '@dfinity/agent';
import borc from 'borc';
import { randomBytes } from '@noble/hashes/utils';
import { bufFromBufLike } from '@dfinity/candid';

function _coseToDerEncodedBlob(cose: ArrayBuffer): DerEncodedPublicKey {
return wrapDER(cose, DER_COSE_OID).buffer as DerEncodedPublicKey;
}

type PublicKeyCredentialWithAttachment = PublicKeyCredential & {
// Extends `PublicKeyCredential` with an optional field introduced in the WebAuthn level 3 spec:
// https://w3c.github.io/webauthn/#dom-publickeycredential-authenticatorattachment
// Already supported by Chrome, Safari and Edge
// Note: `null` is included here as a possible value because Edge set this value to null in the
// past.
authenticatorAttachment: AuthenticatorAttachment | undefined | null;
};

/**
* From the documentation;
* The authData is a byte array described in the spec. Parsing it will involve slicing bytes from
* the array and converting them into usable objects.
*
* See https://webauthn.guide/#registration (subsection "Example: Parsing the authenticator data").
* @param authData The authData field of the attestation response.
* @returns The COSE key of the authData.
*/
function _authDataToCose(authData: ArrayBuffer): ArrayBuffer {
const dataView = new DataView(new ArrayBuffer(2));
const idLenBytes = authData.slice(53, 55);
[...new Uint8Array(idLenBytes)].forEach((v, i) => dataView.setUint8(i, v));
const credentialIdLength = dataView.getUint16(0);

// Get the public key object.
return authData.slice(55 + credentialIdLength);
}

export class CosePublicKey implements PublicKey {
protected _encodedKey: DerEncodedPublicKey;

public constructor(protected _cose: ArrayBuffer) {
this._encodedKey = _coseToDerEncodedBlob(_cose);
}

public toDer(): DerEncodedPublicKey {
return this._encodedKey;
}

public getCose(): ArrayBuffer {
return this._cose;
}
}

/**
* Create a challenge from a string or array. The default challenge is always the same
* because we don't need to verify the authenticity of the key on the server (we don't
* register our keys with the IC). Any challenge would do, even one per key, randomly
* generated.
* @param challenge The challenge to transform into a byte array. By default a hard
* coded string.
*/
function _createChallengeBuffer(challenge: string | Uint8Array = '<ic0.app>'): Uint8Array {
if (typeof challenge === 'string') {
return Uint8Array.from(challenge, c => c.charCodeAt(0));
} else {
return challenge;
}
}

/**
* Create a credentials to authenticate with a server. This is necessary in order in
* WebAuthn to get credentials IDs (which give us the public key and allow us to
* sign), but in the case of the Internet Computer, we don't actually need to register
* it, so we don't.
* @param credentialCreationOptions an optional CredentialCreationOptions object
*/
async function _createCredential(
credentialCreationOptions?: CredentialCreationOptions,
): Promise<PublicKeyCredentialWithAttachment | null> {
const creds = (await navigator.credentials.create(
credentialCreationOptions ?? {
publicKey: {
authenticatorSelection: {
userVerification: 'preferred',
},
attestation: 'direct',
challenge: _createChallengeBuffer(),
pubKeyCredParams: [{ type: 'public-key', alg: PubKeyCoseAlgo.ECDSA_WITH_SHA256 }],
rp: {
name: 'Internet Identity Service',
},
user: {
id: randomBytes(16),
name: 'Internet Identity',
displayName: 'Internet Identity',
},
},
},
)) as PublicKeyCredentialWithAttachment | null;

if (creds === null) {
return null;
}

return {
...creds,
// Some password managers will return a Uint8Array, so we ensure we return an ArrayBuffer.
rawId: bufFromBufLike(creds.rawId),
};
}

// See https://www.iana.org/assignments/cose/cose.xhtml#algorithms for a complete
// list of these algorithms. We only list the ones we support here.
enum PubKeyCoseAlgo {
ECDSA_WITH_SHA256 = -7,
}

/**
* A SignIdentity that uses `navigator.credentials`. See https://webauthn.guide/ for
* more information about WebAuthentication.
*/
export class WebAuthnIdentity extends SignIdentity {
/**
* Create an identity from a JSON serialization.
* @param json - json to parse
*/
public static fromJSON(json: string): WebAuthnIdentity {
const { publicKey, rawId } = JSON.parse(json);

if (typeof publicKey !== 'string' || typeof rawId !== 'string') {
throw new Error('Invalid JSON string.');
}

return new this(fromHex(rawId), fromHex(publicKey), undefined);
}

/**
* Create an identity.
* @param credentialCreationOptions an optional CredentialCreationOptions Challenge
*/
public static async create(
credentialCreationOptions?: CredentialCreationOptions,
): Promise<WebAuthnIdentity> {
const creds = await _createCredential(credentialCreationOptions);

if (!creds || creds.type !== 'public-key') {
throw new Error('Could not create credentials. Error: ' + creds + ' creds JSON: ' + JSON.stringify(creds));
}

const response = creds.response as AuthenticatorAttestationResponse;
if (response.attestationObject === undefined) {
throw new Error('Was expecting an attestation response.');
}

// Parse the attestationObject as CBOR.
const attObject = borc.decodeFirst(new Uint8Array(response.attestationObject));

return new this(
creds.rawId,
_authDataToCose(attObject.authData),
creds.authenticatorAttachment ?? undefined,
);
}

protected _publicKey: CosePublicKey;

public constructor(
public readonly rawId: ArrayBuffer,
cose: ArrayBuffer,
protected authenticatorAttachment: AuthenticatorAttachment | undefined,
) {
super();
this._publicKey = new CosePublicKey(cose);
}

public getPublicKey(): PublicKey {
return this._publicKey;
}

/**
* WebAuthn level 3 spec introduces a new attribute on successful WebAuthn interactions,
* see https://w3c.github.io/webauthn/#dom-publickeycredential-authenticatorattachment.
* This attribute is already implemented for Chrome, Safari and Edge.
*
* Given the attribute is only available after a successful interaction, the information is
* provided opportunistically and might also be `undefined`.
*/
public getAuthenticatorAttachment(): AuthenticatorAttachment | undefined {
return this.authenticatorAttachment;
}

public async sign(blob: ArrayBuffer): Promise<Signature> {
const result = (await navigator.credentials.get({
publicKey: {
allowCredentials: [
{
type: 'public-key',
id: this.rawId,
},
],
challenge: blob,
userVerification: 'preferred',
},
})) as PublicKeyCredentialWithAttachment;

if (result.authenticatorAttachment !== null) {
this.authenticatorAttachment = result.authenticatorAttachment;
}

const response = result.response as AuthenticatorAssertionResponse;

const cbor = borc.encode(
new borc.Tagged(55799, {
authenticator_data: new Uint8Array(response.authenticatorData),
client_data_json: new TextDecoder().decode(response.clientDataJSON),
signature: new Uint8Array(response.signature),
}),
);
if (!cbor) {
throw new Error('failed to encode cbor');
}
return cbor.buffer as Signature;
}

/**
* Allow for JSON serialization of all information needed to reuse this identity.
*/
public toJSON(): JsonnableWebAuthnIdentity {
return {
publicKey: toHex(this._publicKey.getCose()),
rawId: toHex(this.rawId),
};
}
}

/**
* ReturnType<WebAuthnIdentity.toJSON>
*/
export interface JsonnableWebAuthnIdentity {
// The hexadecimal representation of the DER encoded public key.
publicKey: string;
// The string representation of the local WebAuthn Credential.id (base64url encoded).
rawId: string;
}

0 comments on commit 0243040

Please sign in to comment.