finally get something other that simplewebauthn working
This commit is contained in:
102
src/libs/crypto/passkeyHelpers.ts
Normal file
102
src/libs/crypto/passkeyHelpers.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
|
||||
import { AsnParser } from "@peculiar/asn1-schema";
|
||||
import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
||||
|
||||
/**
|
||||
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
|
||||
*
|
||||
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
|
||||
*/
|
||||
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
||||
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
|
||||
let rBytes = new Uint8Array(parsedSignature.r);
|
||||
let sBytes = new Uint8Array(parsedSignature.s);
|
||||
|
||||
if (shouldRemoveLeadingZero(rBytes)) {
|
||||
rBytes = rBytes.slice(1);
|
||||
}
|
||||
|
||||
if (shouldRemoveLeadingZero(sBytes)) {
|
||||
sBytes = sBytes.slice(1);
|
||||
}
|
||||
|
||||
const finalSignature = isoUint8ArrayConcat([rBytes, sBytes]);
|
||||
|
||||
return finalSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
|
||||
* should be removed based on the following logic:
|
||||
*
|
||||
* "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
|
||||
* then remove the leading 0x0 byte"
|
||||
*/
|
||||
function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
||||
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
|
||||
}
|
||||
|
||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49
|
||||
/**
|
||||
* Combine multiple Uint8Arrays into a single Uint8Array
|
||||
*/
|
||||
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
|
||||
let pointer = 0;
|
||||
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
|
||||
|
||||
const toReturn = new Uint8Array(totalLength);
|
||||
|
||||
arrays.forEach((arr) => {
|
||||
toReturn.set(arr, pointer);
|
||||
pointer += arr.length;
|
||||
});
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
|
||||
let webCrypto: unknown = undefined;
|
||||
export function getWebCrypto() {
|
||||
/**
|
||||
* Hello there! If you came here wondering why this method is asynchronous when use of
|
||||
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
|
||||
* synchronous. For example, `generateRegistrationOptions()` and `generateAuthenticationOptions()`
|
||||
* become synchronous if we make this synchronous (since nothing else in that method is async)
|
||||
* which represents a breaking API change in this library's core API.
|
||||
*
|
||||
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense
|
||||
* to keep this method asynchronous.
|
||||
*/
|
||||
const toResolve = new Promise((resolve, reject) => {
|
||||
if (webCrypto) {
|
||||
return resolve(webCrypto);
|
||||
}
|
||||
/**
|
||||
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
|
||||
* support (and Node v20+)
|
||||
*/
|
||||
const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto();
|
||||
if (_globalThisCrypto) {
|
||||
webCrypto = _globalThisCrypto;
|
||||
return resolve(webCrypto);
|
||||
}
|
||||
// We tried to access it both in Node and globally, so bail out
|
||||
return reject(new MissingWebCrypto());
|
||||
});
|
||||
return toResolve;
|
||||
}
|
||||
export class MissingWebCrypto extends Error {
|
||||
constructor() {
|
||||
const message = "An instance of the Crypto API could not be located";
|
||||
super(message);
|
||||
this.name = "MissingWebCrypto";
|
||||
}
|
||||
}
|
||||
// Make it possible to stub return values during testing
|
||||
export const _getWebCryptoInternals = {
|
||||
stubThisGlobalThisCrypto: () => globalThis.crypto,
|
||||
// Make it possible to reset the `webCrypto` at the top of the file
|
||||
setCachedCrypto: (newCrypto: unknown) => {
|
||||
webCrypto = newCrypto;
|
||||
},
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
import asn1 from "asn1-ber";
|
||||
import { Buffer } from "buffer/";
|
||||
import { decode as cborDecode } from "cbor-x";
|
||||
import { createJWS, JWTPayload, verifyJWT } from "did-jwt";
|
||||
import { DIDResolutionResult, Resolver } from "did-resolver";
|
||||
import { createJWS, JWTPayload } from "did-jwt";
|
||||
import { DIDResolutionResult } from "did-resolver";
|
||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||
import { bytesToMultibase } from "@veramo/utils";
|
||||
import {
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
} from "@simplewebauthn/types";
|
||||
|
||||
import { generateRandomBytes } from "@/libs/crypto";
|
||||
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
|
||||
|
||||
export interface JWK {
|
||||
kty: string;
|
||||
@@ -77,12 +78,24 @@ export async function registerCredential(userId: Uint8Array) {
|
||||
expectedRPID: window.location.hostname,
|
||||
});
|
||||
console.log("verification", verification);
|
||||
const jwkObj = cborDecode(
|
||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||
);
|
||||
console.log("jwkObj from verification", jwkObj);
|
||||
console.log(
|
||||
"[1]==2 => kty EC",
|
||||
"[3]==-7 => alg ES256",
|
||||
"[-1]==1 => crv P-256",
|
||||
);
|
||||
const { publicKeyJwk } = cborToKeys(
|
||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||
);
|
||||
|
||||
return {
|
||||
authData: verification.registrationInfo?.attestationObject,
|
||||
credId: verification.registrationInfo?.credentialID as string,
|
||||
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")),
|
||||
publicKeyJwk: undefined,
|
||||
publicKeyJwk: publicKeyJwk,
|
||||
publicKeyBytes: verification.registrationInfo
|
||||
?.credentialPublicKey as Uint8Array,
|
||||
};
|
||||
@@ -153,30 +166,9 @@ export async function registerCredential2(userId: Uint8Array) {
|
||||
);
|
||||
console.log("attestationObject", attestationObject);
|
||||
|
||||
const jwkObj = cborDecode(
|
||||
const { publicKeyJwk, publicKeyBuffer } = cborToKeys(
|
||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||
);
|
||||
console.log("jwkObj from verification", jwkObj);
|
||||
if (
|
||||
jwkObj[1] != 2 || // kty "EC"
|
||||
jwkObj[3] != -7 || // alg "ES256"
|
||||
jwkObj[-1] != 1 || // crv "P-256"
|
||||
jwkObj[-2].length != 32 || // x
|
||||
jwkObj[-3].length != 32 // y
|
||||
) {
|
||||
throw new Error("Unable to extract key.");
|
||||
}
|
||||
const publicKeyJwk = {
|
||||
alg: "ES256",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
x: arrayToBase64Url(jwkObj[-2]),
|
||||
y: arrayToBase64Url(jwkObj[-3]),
|
||||
};
|
||||
const publicKeyBytes = Buffer.concat([
|
||||
Buffer.from(jwkObj[-2]),
|
||||
Buffer.from(jwkObj[-3]),
|
||||
]);
|
||||
|
||||
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
||||
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
||||
@@ -186,7 +178,7 @@ export async function registerCredential2(userId: Uint8Array) {
|
||||
credId: credential?.id,
|
||||
rawId: credential?.rawId,
|
||||
publicKeyJwk,
|
||||
publicKeyBytes,
|
||||
publicKeyBytes: publicKeyBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -208,9 +200,12 @@ export function createPeerDid(publicKeyBytes: Uint8Array) {
|
||||
|
||||
export class PeerSetup {
|
||||
public authenticatorData?: ArrayBuffer;
|
||||
public authenticatorDataBase64Url?: Base64URLString;
|
||||
public challenge?: Uint8Array;
|
||||
public clientDataJsonDecoded?: object;
|
||||
public clientDataJsonBase64Url?: Base64URLString;
|
||||
public signature?: Base64URLString;
|
||||
public publicKeyJwk?: JWK;
|
||||
|
||||
public async createJwt(
|
||||
payload: object,
|
||||
@@ -244,16 +239,23 @@ export class PeerSetup {
|
||||
const clientAuth = await startAuthentication(options);
|
||||
console.log("custom clientAuth", clientAuth);
|
||||
|
||||
this.authenticatorDataBase64Url = clientAuth.response.authenticatorData;
|
||||
this.authenticatorData = Buffer.from(
|
||||
clientAuth.response.authenticatorData,
|
||||
"base64",
|
||||
).buffer;
|
||||
this.challenge = payloadHash;
|
||||
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
|
||||
this.clientDataJsonDecoded = JSON.parse(
|
||||
Buffer.from(clientAuth.response.clientDataJSON, "base64").toString(
|
||||
"utf-8",
|
||||
),
|
||||
);
|
||||
console.log("authenticatorData for signing", this.authenticatorData);
|
||||
console.log(
|
||||
"clientDataJSON for signing",
|
||||
Buffer.from(clientAuth.response.clientDataJSON, "base64"),
|
||||
);
|
||||
this.signature = clientAuth.response.signature;
|
||||
|
||||
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64");
|
||||
@@ -373,9 +375,12 @@ export async function verifyJwt(
|
||||
credId: Base64URLString,
|
||||
rawId: Uint8Array,
|
||||
authenticatorData: ArrayBuffer,
|
||||
authenticatorDataBase64Url: Base64URLString,
|
||||
challenge: Uint8Array,
|
||||
clientDataJSON: object,
|
||||
publicKey: Uint8Array,
|
||||
clientDataJsonBase64Url: Base64URLString,
|
||||
publicKeyBytes: Uint8Array,
|
||||
publicKeyJwk: JWK,
|
||||
signature: Base64URLString,
|
||||
) {
|
||||
// Here's a combined auth & verify process, based on some of the inputs.
|
||||
@@ -397,7 +402,7 @@ export async function verifyJwt(
|
||||
// const verfOpts: VerifyAuthenticationResponseOpts = {
|
||||
// authenticator: {
|
||||
// credentialID: credId,
|
||||
// credentialPublicKey: publicKey,
|
||||
// credentialPublicKey: publicKeyBytes,
|
||||
// counter: 0,
|
||||
// },
|
||||
// expectedChallenge: options.challenge,
|
||||
@@ -413,7 +418,7 @@ export async function verifyJwt(
|
||||
const authOpts: VerifyAuthenticationResponseOpts = {
|
||||
authenticator: {
|
||||
credentialID: credId,
|
||||
credentialPublicKey: publicKey,
|
||||
credentialPublicKey: publicKeyBytes,
|
||||
counter: 0,
|
||||
},
|
||||
expectedChallenge: arrayToBase64Url(challenge),
|
||||
@@ -438,10 +443,107 @@ export async function verifyJwt(
|
||||
const verification = await verifyAuthenticationResponse(authOpts);
|
||||
console.log("auth verification", verification);
|
||||
|
||||
const decoded = verifyJWT(jwt, {
|
||||
resolver: new Resolver({ peer: peerDidToDidDocument }),
|
||||
});
|
||||
return decoded;
|
||||
// It doesn't work to use the did-jwt verifyJWT with did-resolver Resolver
|
||||
// const decoded = verifyJWT(jwt, {
|
||||
// resolver: new Resolver({ peer: peerDidToDidDocument }),
|
||||
// });
|
||||
// return decoded;
|
||||
|
||||
// const [headerB64, concatenatedPayloadB64, signatureB64] = jwt.split(".");
|
||||
//
|
||||
// const header = JSON.parse(atob(headerB64));
|
||||
// const jsonPayload = JSON.parse(atob(concatenatedPayloadB64));
|
||||
// const [jsonPayloadB64, otherDataHashB64] = concatenatedPayloadB64.split(".");
|
||||
//
|
||||
// const otherDataHash = base64urlDecode(otherDataHashB64);
|
||||
// const signature = base64urlDecode(signatureB64);
|
||||
//
|
||||
// const dataToVerify = new TextEncoder().encode(`${headerB64}.${concatenatedPayloadB64}`);
|
||||
// const dataHash = await sha256(dataToVerify);
|
||||
//
|
||||
// const authenticatorData = base64urlDecode(jsonPayload.authenticatorData);
|
||||
// const clientDataJSON = jsonPayload.clientDataJSON;
|
||||
|
||||
/////////
|
||||
|
||||
// const clientBuffer = clientDataJsonBase64Url
|
||||
// .replace(/-/g, "+")
|
||||
// .replace(/_/g, "/");
|
||||
// const clientData = new Uint8Array(Buffer.from(clientBuffer, "base64"));
|
||||
// const clientDataHash = sha256(clientData);
|
||||
//
|
||||
// const verifyData = new Uint8Array([
|
||||
// ...new Uint8Array(authenticatorData),
|
||||
// ...new Uint8Array(clientDataHash),
|
||||
// ]);
|
||||
// console.log("verifyData", verifyData);
|
||||
// const sigBase64Raw = signature.replace(/-/g, "+").replace(/_/g, "/");
|
||||
//
|
||||
// const sigHex = new Uint8Array(Buffer.from(sigBase64Raw, "base64")); //Buffer.from(sigBase64Raw, "base64").toString("hex");
|
||||
// const msgHex = verifyData; //new Buffer(verifyData).toString("hex");
|
||||
// const pubHex = publicKeyBytes; //new Buffer(publicKeyBytes).toString("hex");
|
||||
// console.log("sig msg pub", sigHex, msgHex, pubHex);
|
||||
// const isValid = p256.verify(sigHex, msgHex, pubHex);
|
||||
|
||||
/////////
|
||||
|
||||
const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64");
|
||||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||
const sigBuffer = Buffer.from(signature, "base64");
|
||||
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||
|
||||
// Hash the client data
|
||||
const hash = sha256(clientDataFromBase);
|
||||
|
||||
// Construct the preimage
|
||||
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||
|
||||
console.log("finalSigBuffer", finalSigBuffer);
|
||||
console.log("preimage", preimage);
|
||||
console.log("publicKeyBytes", publicKeyBytes);
|
||||
|
||||
// This uses p256 from @noble/curves/p256, which I would prefer but it's returning false.
|
||||
// const isValid = p256.verify(
|
||||
// finalSigBuffer,
|
||||
// new Uint8Array(preimage),
|
||||
// publicKeyBytes,
|
||||
// );
|
||||
// console.log("isValid", isValid);
|
||||
|
||||
/////////
|
||||
|
||||
const WebCrypto = await getWebCrypto();
|
||||
const verifyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
hash: { name: "SHA-256" },
|
||||
};
|
||||
const keyAlgorithm = {
|
||||
name: "ECDSA",
|
||||
namedCurve: publicKeyJwk.crv,
|
||||
};
|
||||
// const publicKeyCryptoKey = await importKey({
|
||||
// publicKeyJwk,
|
||||
// algorithm: keyAlgorithm,
|
||||
// });
|
||||
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
||||
"jwk",
|
||||
publicKeyJwk,
|
||||
keyAlgorithm,
|
||||
false,
|
||||
["verify"],
|
||||
);
|
||||
console.log("verifyAlgorithm", verifyAlgorithm);
|
||||
console.log("publicKeyCryptoKey", publicKeyCryptoKey);
|
||||
console.log("finalSigBuffer", finalSigBuffer);
|
||||
console.log("preimage", preimage);
|
||||
const verified = await WebCrypto.subtle.verify(
|
||||
verifyAlgorithm,
|
||||
publicKeyCryptoKey,
|
||||
finalSigBuffer,
|
||||
preimage,
|
||||
);
|
||||
console.log("verified", verified);
|
||||
return verified;
|
||||
}
|
||||
|
||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
@@ -481,3 +583,86 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
||||
};
|
||||
}
|
||||
|
||||
// convert COSE public key to PEM format
|
||||
function COSEtoPEM(cose: Buffer) {
|
||||
// const alg = cose.get(3); // Algorithm
|
||||
const x = cose[-2]; // x-coordinate
|
||||
const y = cose[-3]; // y-coordinate
|
||||
|
||||
// Ensure the coordinates are in the correct format
|
||||
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
||||
|
||||
// Convert to PEM format
|
||||
const pem = `-----BEGIN PUBLIC KEY-----
|
||||
${pubKeyBuffer.toString("base64")}
|
||||
-----END PUBLIC KEY-----`;
|
||||
|
||||
return pem;
|
||||
}
|
||||
|
||||
function base64urlDecode(input: string) {
|
||||
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||
const str = atob(input + pad);
|
||||
const bytes = new Uint8Array(str.length);
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
bytes[i] = str.charCodeAt(i);
|
||||
}
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
function base64urlEncode(buffer: ArrayBuffer) {
|
||||
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||
}
|
||||
|
||||
function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||
const jwkObj = cborDecode(publicKeyBytes);
|
||||
console.log("jwkObj from verification", jwkObj);
|
||||
if (
|
||||
jwkObj[1] != 2 || // kty "EC"
|
||||
jwkObj[3] != -7 || // alg "ES256"
|
||||
jwkObj[-1] != 1 || // crv "P-256"
|
||||
jwkObj[-2].length != 32 || // x
|
||||
jwkObj[-3].length != 32 // y
|
||||
) {
|
||||
throw new Error("Unable to extract key.");
|
||||
}
|
||||
const publicKeyJwk = {
|
||||
alg: "ES256",
|
||||
crv: "P-256",
|
||||
kty: "EC",
|
||||
x: arrayToBase64Url(jwkObj[-2]),
|
||||
y: arrayToBase64Url(jwkObj[-3]),
|
||||
};
|
||||
const publicKeyBuffer = Buffer.concat([
|
||||
Buffer.from(jwkObj[-2]),
|
||||
Buffer.from(jwkObj[-3]),
|
||||
]);
|
||||
return { publicKeyJwk, publicKeyBuffer };
|
||||
}
|
||||
|
||||
async function pemToCryptoKey(pem: string) {
|
||||
const binaryDerString = atob(
|
||||
pem
|
||||
.split("\n")
|
||||
.filter((x) => !x.includes("-----"))
|
||||
.join(""),
|
||||
);
|
||||
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||
for (let i = 0; i < binaryDerString.length; i++) {
|
||||
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||
}
|
||||
console.log("binaryDer", binaryDer.buffer);
|
||||
return await window.crypto.subtle.importKey(
|
||||
"spki",
|
||||
binaryDer.buffer,
|
||||
{
|
||||
name: "RSASSA-PKCS1-v1_5",
|
||||
hash: "SHA-256",
|
||||
},
|
||||
true,
|
||||
["verify"],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,9 +265,7 @@ export default class Help extends Vue {
|
||||
const encodedUserId = Buffer.from(this.userId).toString("base64");
|
||||
console.log("encodedUserId", encodedUserId);
|
||||
|
||||
const cred = await registerCredential(
|
||||
this.userId as Uint8Array
|
||||
);
|
||||
const cred = await registerCredential(this.userId as Uint8Array);
|
||||
console.log("public key", cred);
|
||||
this.publicKeyJwk = cred.publicKeyJwk;
|
||||
this.publicKeyBytes = cred.publicKeyBytes;
|
||||
@@ -277,6 +275,7 @@ export default class Help extends Vue {
|
||||
}
|
||||
|
||||
public async create() {
|
||||
console.log("Starting a create");
|
||||
const did = createPeerDid(this.publicKeyBytes as Uint8Array);
|
||||
console.log("did", did);
|
||||
this.peerSetup = new PeerSetup();
|
||||
@@ -303,9 +302,12 @@ export default class Help extends Vue {
|
||||
this.credId as Base64URLString,
|
||||
this.rawId as Uint8Array,
|
||||
this.peerSetup.authenticatorData as ArrayBuffer,
|
||||
this.peerSetup.authenticatorDataBase64Url as Base64URLString,
|
||||
this.peerSetup.challenge as Uint8Array,
|
||||
this.peerSetup.clientDataJsonDecoded,
|
||||
this.peerSetup.clientDataJsonBase64Url as Base64URLString,
|
||||
this.publicKeyBytes as Uint8Array,
|
||||
this.publicKeyJwk as JWK,
|
||||
this.peerSetup.signature as Base64URLString,
|
||||
);
|
||||
console.log("decoded", decoded);
|
||||
|
||||
Reference in New Issue
Block a user