refactor, and make separate testing buttons for different approaches

This commit is contained in:
2024-06-15 15:45:29 -06:00
parent bbf1c17e62
commit 141a7fd563
2 changed files with 309 additions and 396 deletions

View File

@@ -1,20 +1,19 @@
import asn1 from "asn1-ber"; import asn1 from "asn1-ber";
import { Buffer } from "buffer/"; import { Buffer } from "buffer/";
import { decode as cborDecode } from "cbor-x"; import { decode as cborDecode } from "cbor-x";
import { createJWS, JWTPayload } from "did-jwt"; import { JWTPayload } from "did-jwt";
import { DIDResolutionResult } from "did-resolver"; import { DIDResolutionResult } from "did-resolver";
import { sha256 } from "ethereum-cryptography/sha256.js"; import { sha256 } from "ethereum-cryptography/sha256.js";
import { bytesToMultibase } from "@veramo/utils"; import { bytesToMultibase } from "@veramo/utils";
import { import {
startAuthentication, startAuthentication,
startRegistration, WebAuthnAbortService, startRegistration,
} from "@simplewebauthn/browser"; } from "@simplewebauthn/browser";
import { import {
generateAuthenticationOptions, generateAuthenticationOptions,
generateRegistrationOptions, generateRegistrationOptions,
verifyAuthenticationResponse, verifyAuthenticationResponse,
verifyRegistrationResponse, verifyRegistrationResponse,
VerifyRegistrationResponseOpts,
} from "@simplewebauthn/server"; } from "@simplewebauthn/server";
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse";
import { import {
@@ -23,7 +22,6 @@ import {
PublicKeyCredentialRequestOptionsJSON, PublicKeyCredentialRequestOptionsJSON,
} from "@simplewebauthn/types"; } from "@simplewebauthn/types";
import { generateRandomBytes } from "@/libs/crypto";
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
export interface JWK { export interface JWK {
@@ -54,13 +52,6 @@ export async function registerCredential(userId: Uint8Array) {
// Don't prompt users for additional information about the authenticator // Don't prompt users for additional information about the authenticator
// (Recommended for smoother UX) // (Recommended for smoother UX)
attestationType: "none", attestationType: "none",
// Prevent users from re-registering existing authenticators
// excludeCredentials: userPasskeys.map(passkey => ({
// id: passkey.id,
// // Optional
// transports: passkey.transports,
// })),
// // See "Guiding use of authenticators via authenticatorSelection" below
authenticatorSelection: { authenticatorSelection: {
// Defaults // Defaults
residentKey: "preferred", residentKey: "preferred",
@@ -69,24 +60,21 @@ export async function registerCredential(userId: Uint8Array) {
authenticatorAttachment: "platform", authenticatorAttachment: "platform",
}, },
}); });
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
// with pubKeyCredParams: { type: "public-key", alg: -7 }
const attResp = await startRegistration(options); const attResp = await startRegistration(options);
console.log("attResp", attResp);
const verification = await verifyRegistrationResponse({ const verification = await verifyRegistrationResponse({
response: attResp, response: attResp,
expectedChallenge: options.challenge, expectedChallenge: options.challenge,
expectedOrigin: window.location.origin, expectedOrigin: window.location.origin,
expectedRPID: window.location.hostname, expectedRPID: window.location.hostname,
}); });
console.log("verification", verification);
const jwkObj = cborDecode( // references for parsing auth data and getting the public key
verification.registrationInfo?.credentialPublicKey as Uint8Array, // https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
); // https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
console.log("jwkObj from verification", jwkObj); // https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
console.log(
"[1]==2 => kty EC",
"[3]==-7 => alg ES256",
"[-1]==1 => crv P-256",
);
const { publicKeyJwk } = cborToKeys( const { publicKeyJwk } = cborToKeys(
verification.registrationInfo?.credentialPublicKey as Uint8Array, verification.registrationInfo?.credentialPublicKey as Uint8Array,
); );
@@ -101,99 +89,13 @@ export async function registerCredential(userId: Uint8Array) {
}; };
} }
export async function registerCredential2(userId: Uint8Array) {
const challenge = generateRandomBytes(32);
const publicKeyOptions: PublicKeyCredentialCreationOptions = {
challenge: challenge,
rp: {
name: "Time Safari",
id: window.location.hostname,
},
user: {
id: userId,
name: "Current-User",
displayName: "Current User",
},
pubKeyCredParams: [
{
type: "public-key",
alg: -7, // ES256 algorithm
},
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "preferred",
},
timeout: 60000,
attestation: "direct",
};
const credential = await navigator.credentials.create({
publicKey: publicKeyOptions,
});
console.log("credential", credential);
console.log(credential?.id, " is the new ID base64-url-encoded");
console.log(arrayToBase64Url(credential?.rawId), " is the base64 rawId");
const attestationResponse = credential?.response;
const verfInput: VerifyRegistrationResponseOpts = {
response: {
id: credential?.id as string,
rawId: credential?.id as string, //Buffer.from(credential?.rawId).toString("base64"),
response: {
attestationObject: arrayToBase64Url(attestationResponse?.attestationObject),
clientDataJSON: arrayToBase64Url(attestationResponse?.clientDataJSON),
},
clientExtensionResults: {},
type: "public-key",
},
expectedChallenge: arrayToBase64Url(challenge),
expectedOrigin: window.location.origin,
};
console.log("verfInput", verfInput);
const verification = await verifyRegistrationResponse(verfInput);
console.log("verification", verification);
// Parse the attestation response to get the public key
const clientDataJSON = attestationResponse.clientDataJSON;
console.log("clientDataJSON raw", clientDataJSON);
console.log(
"clientDataJSON dec",
new TextDecoder("utf-8").decode(clientDataJSON),
);
const attestationObject = cborDecode(
new Uint8Array(attestationResponse.attestationObject),
);
console.log("attestationObject", attestationObject);
const { publicKeyJwk, publicKeyBuffer } = cborToKeys(
verification.registrationInfo?.credentialPublicKey as Uint8Array,
);
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
return {
authData: attestationObject.authData,
credId: credential?.id,
rawId: credential?.rawId,
publicKeyJwk,
publicKeyBytes: publicKeyBuffer,
};
}
// parse authData
// here's one: https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
// from https://chatgpt.com/c/0ce72fda-bc5d-42ff-a748-6022f6e39fa0
// from https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
export function createPeerDid(publicKeyBytes: Uint8Array) { export function createPeerDid(publicKeyBytes: Uint8Array) {
// https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67 // https://github.com/decentralized-identity/veramo/blob/next/packages/did-provider-peer/src/peer-did-provider.ts#L67
//const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME }); //const provider = new PeerDIDProvider({ defaultKms: LOCAL_KMS_NAME });
const methodSpecificId = bytesToMultibase( const methodSpecificId = bytesToMultibase(
publicKeyBytes, publicKeyBytes,
"base58btc", "base58btc",
"secp256k1-pub", "p256-pub",
); );
return "did:peer:0" + methodSpecificId; return "did:peer:0" + methodSpecificId;
} }
@@ -205,11 +107,11 @@ export class PeerSetup {
public clientDataJsonDecoded?: object; public clientDataJsonDecoded?: object;
public clientDataJsonBase64Url?: Base64URLString; public clientDataJsonBase64Url?: Base64URLString;
public signature?: Base64URLString; public signature?: Base64URLString;
public publicKeyJwk?: JWK;
public async createJwt(fullPayload: object, credentialId: string) {
const header: JWTPayload = { typ: "JWT", alg: "ES256" };
public async createJwtSimplewebauthn(
fullPayload: object,
credentialId: string,
) {
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload)));
// const payloadHash: Uint8Array = sha256(this.challenge); // const payloadHash: Uint8Array = sha256(this.challenge);
const options: PublicKeyCredentialRequestOptionsJSON = const options: PublicKeyCredentialRequestOptionsJSON =
@@ -233,83 +135,13 @@ export class PeerSetup {
"utf-8", "utf-8",
), ),
); );
//console.log("simple authenticatorData for signing", this.authenticatorData); // console.log("simple authenticatorData for signing", this.authenticatorData);
this.signature = clientAuth.response.signature; this.signature = clientAuth.response.signature;
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
const payloadBase64 = clientAuth.response.clientDataJSON; const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
const signature = clientAuth.response.signature; const headerBase64 = Buffer.from(JSON.stringify(header))
.toString("base64")
return headerBase64 + "." + payloadBase64 + "." + signature;
}
public async createJwt2(fullPayload: object, credentialId: string) {
const header: JWTPayload = { typ: "JWT", alg: "ES256" };
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64");
const dataToSignString = JSON.stringify(fullPayload);
const dataToSignBuffer = Buffer.from(dataToSignString);
//console.log("lower credentialId", credentialId);
this.challenge = new Uint8Array(dataToSignBuffer);
const options = {
publicKey: {
challenge: this.challenge.buffer,
rpID: window.location.hostname,
//allowCredentials: [{ id: credentialId, type: "public-key" }],
userVerification: "preferred",
//extensions: fullPayload,
},
};
// console.log("lower authentication options", options);
// console.log("lower options in base64", {
// publicKey: {
// challenge: bufferToBase64URLString(options.publicKey.challenge),
// rpID: window.location.hostname,
// userVerification: "preferred",
// },
// });
const credential = await navigator.credentials.get(options);
// console.log("lower credential get", credential);
// console.log("lower credential get in base64", {
// id: credential?.id,
// rawId: bufferToBase64URLString(credential?.rawId),
// response: {
// authenticatorData: bufferToBase64URLString(
// credential?.response.authenticatorData,
// ),
// clientDataJSON: bufferToBase64URLString(
// credential?.response.clientDataJSON,
// ),
// signature: bufferToBase64URLString(credential?.response.signature),
// },
// type: credential?.type,
// });
const authenticatorAssertionResponse = credential?.response;
this.authenticatorDataBase64Url =
authenticatorAssertionResponse.authenticatorData;
this.authenticatorData = Buffer.from(
this.authenticatorDataBase64Url as Base64URLString,
"base64",
).buffer;
// console.log("lower authenticator data", this.authenticatorData);
this.clientDataJsonBase64Url =
authenticatorAssertionResponse.clientDataJSON;
this.clientDataJsonDecoded = JSON.parse(
new TextDecoder("utf-8").decode(
authenticatorAssertionResponse.clientDataJSON,
),
);
// console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded);
const origSignature = Buffer.from(
authenticatorAssertionResponse.signature,
).toString("base64");
this.signature = origSignature
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_") .replace(/\//g, "_")
.replace(/=+$/, ""); .replace(/=+$/, "");
@@ -320,121 +152,131 @@ export class PeerSetup {
}; };
const dataInJwtString = JSON.stringify(dataInJwt); const dataInJwtString = JSON.stringify(dataInJwt);
const payloadBase64 = Buffer.from(dataInJwtString).toString("base64"); const payloadBase64 = Buffer.from(dataInJwtString).toString("base64");
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
return jwt; const signature = clientAuth.response.signature;
return headerBase64 + "." + payloadBase64 + "." + signature;
} }
// Attempted with JWS, but it will not match because it signs different content (header + payload) public async createJwtNavigator(fullPayload: object, credentialId: string) {
//const signer = await this.webAuthnES256KSigner(credentialId); const dataToSignString = JSON.stringify(fullPayload);
//const jwt = createJWS(fullPayload, signer, { typ: "JWT", alg: "ES256" }); const dataToSignBuffer = Buffer.from(dataToSignString);
async webAuthnES256KSigner(credentialID: string) {
return async (data: string | Uint8Array) => {
const signature = await this.generateWebAuthnSignature(
data,
credentialID,
);
// This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library. // console.log("lower credentialId", credentialId);
const signatureBuffer = Buffer.from(signature); this.challenge = new Uint8Array(dataToSignBuffer);
console.log("lower signature inside signer", signature);
console.log("lower buffer signature inside signer", signatureBuffer);
console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64"));
// Decode the DER-encoded signature to extract R and S values
const reader = new asn1.BerReader(signatureBuffer);
console.log("lower after reader");
reader.readSequence();
console.log("lower after read sequence");
const r = reader.readString(asn1.Ber.Integer, true);
console.log("lower after r");
const s = reader.readString(asn1.Ber.Integer, true);
console.log("lower after r & s");
// Ensure R and S are 32 bytes each
const rBuffer = Buffer.from(r);
const sBuffer = Buffer.from(s);
console.log("lower after rBuffer & sBuffer", rBuffer, sBuffer);
const rWithoutPrefix = rBuffer.length > 32 ? rBuffer.slice(1) : rBuffer;
const sWithoutPrefix = sBuffer.length > 32 ? sBuffer.slice(1) : sBuffer;
const rPadded =
rWithoutPrefix.length < 32
? Buffer.concat([Buffer.alloc(32 - rWithoutPrefix.length), rBuffer])
: rWithoutPrefix;
const sPadded =
rWithoutPrefix.length < 32
? Buffer.concat([Buffer.alloc(32 - sWithoutPrefix.length), sBuffer])
: sWithoutPrefix;
// Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
const combinedSignature = Buffer.concat([rPadded, sPadded]);
console.log(
"lower combinedSignature",
combinedSignature.length,
combinedSignature,
);
const combSig64 = combinedSignature.toString("base64");
console.log("lower combSig64", combSig64);
const combSig64Url = combSig64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
console.log("lower combSig64Url", combSig64Url);
return combSig64Url;
};
}
async generateWebAuthnSignature(
dataToSign: string | Uint8Array, // from Signer interface
credentialId: string,
) {
if (!(dataToSign instanceof Uint8Array)) {
console.log("lower dataToSign & typeof ", typeof dataToSign, dataToSign);
dataToSign = new Uint8Array(base64URLStringToBuffer(dataToSign));
}
console.log("lower credentialId", credentialId);
this.challenge = dataToSign;
const options = { const options = {
publicKey: { publicKey: {
challenge: this.challenge.buffer, challenge: this.challenge.buffer,
rpID: window.location.hostname, rpID: window.location.hostname,
//allowCredentials: [{ id: credentialId, type: "public-key" }],
userVerification: "preferred", userVerification: "preferred",
//extensions: fullPayload,
}, },
}; };
// console.log("lower authentication options", options); const credential = await navigator.credentials.get(options);
const assertion = await navigator.credentials.get(options); // console.log("nav credential get", credential);
// console.log("lower credential get", assertion);
const authenticatorAssertionResponse = assertion?.response; this.authenticatorDataBase64Url = bufferToBase64URLString(
credential?.response.authenticatorData,
this.authenticatorDataBase64Url = );
authenticatorAssertionResponse.authenticatorData;
this.authenticatorData = Buffer.from( this.authenticatorData = Buffer.from(
this.authenticatorDataBase64Url as Base64URLString, this.authenticatorDataBase64Url as Base64URLString,
"base64", "base64",
).buffer; ).buffer;
// console.log("lower authenticator data", this.authenticatorData);
this.clientDataJsonBase64Url = this.clientDataJsonBase64Url = bufferToBase64URLString(
authenticatorAssertionResponse.clientDataJSON; credential?.response.clientDataJSON,
this.clientDataJsonDecoded = JSON.parse( );
new TextDecoder("utf-8").decode( this.clientDataJsonDecoded = JSON.parse(
authenticatorAssertionResponse.clientDataJSON, new TextDecoder("utf-8").decode(credential?.response.clientDataJSON),
),
); );
// console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded);
this.signature = Buffer.from( // Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
authenticatorAssertionResponse.signature, const header: JWTPayload = { typ: "JWANT", alg: "ES256" };
).toString("base64"); const headerBase64 = Buffer.from(JSON.stringify(header))
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
return this.signature; const dataInJwt = {
AuthenticationData: this.authenticatorDataBase64Url,
ClientDataJSON: this.clientDataJsonBase64Url,
};
const dataInJwtString = JSON.stringify(dataInJwt);
const payloadBase64 = Buffer.from(dataInJwtString)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const origSignature = Buffer.from(credential?.response.signature)
.toString("base64")
this.signature = origSignature
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature;
return jwt;
} }
// return a low-level signing function, similar to createJWS approach
// async webAuthnES256KSigner(credentialID: string) {
// return async (data: string | Uint8Array) => {
// // get signature from WebAuthn
// const signature = await this.generateWebAuthnSignature(data);
//
// // This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library.
// const signatureBuffer = Buffer.from(signature);
// console.log("lower signature inside signer", signature);
// console.log("lower buffer signature inside signer", signatureBuffer);
// console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64"));
// // Decode the DER-encoded signature to extract R and S values
// const reader = new asn1.BerReader(signatureBuffer);
// console.log("lower after reader");
// reader.readSequence();
// console.log("lower after read sequence");
// const r = reader.readString(asn1.Ber.Integer, true);
// console.log("lower after r");
// const s = reader.readString(asn1.Ber.Integer, true);
// console.log("lower after r & s");
//
// // Ensure R and S are 32 bytes each
// const rBuffer = Buffer.from(r);
// const sBuffer = Buffer.from(s);
// console.log("lower after rBuffer & sBuffer", rBuffer, sBuffer);
// const rWithoutPrefix = rBuffer.length > 32 ? rBuffer.slice(1) : rBuffer;
// const sWithoutPrefix = sBuffer.length > 32 ? sBuffer.slice(1) : sBuffer;
// const rPadded =
// rWithoutPrefix.length < 32
// ? Buffer.concat([Buffer.alloc(32 - rWithoutPrefix.length), rBuffer])
// : rWithoutPrefix;
// const sPadded =
// rWithoutPrefix.length < 32
// ? Buffer.concat([Buffer.alloc(32 - sWithoutPrefix.length), sBuffer])
// : sWithoutPrefix;
//
// // Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
// const combinedSignature = Buffer.concat([rPadded, sPadded]);
// console.log(
// "lower combinedSignature",
// combinedSignature.length,
// combinedSignature,
// );
//
// const combSig64 = combinedSignature.toString("base64");
// console.log("lower combSig64", combSig64);
// const combSig64Url = combSig64
// .replace(/\+/g, "-")
// .replace(/\//g, "_")
// .replace(/=+$/, "");
// console.log("lower combSig64Url", combSig64Url);
// return combSig64Url;
// };
// }
} }
export async function verifyJwt( export async function verifyJwtSimplewebauthn(
jwt: string, jwt: string,
credId: Base64URLString, credId: Base64URLString,
rawId: Uint8Array, rawId: Uint8Array,
@@ -447,35 +289,53 @@ export async function verifyJwt(
publicKeyJwk: JWK, publicKeyJwk: JWK,
signature: Base64URLString, signature: Base64URLString,
) { ) {
// const authData = arrayToBase64Url(Buffer.from(authenticatorData)); const authData = arrayToBase64Url(Buffer.from(authenticatorData));
// const authOpts: VerifyAuthenticationResponseOpts = { const authOpts: VerifyAuthenticationResponseOpts = {
// authenticator: { authenticator: {
// credentialID: credId, credentialID: credId,
// credentialPublicKey: publicKeyBytes, credentialPublicKey: publicKeyBytes,
// counter: 0, counter: 0,
// }, },
// expectedChallenge: arrayToBase64Url(challenge), expectedChallenge: arrayToBase64Url(challenge),
// expectedOrigin: window.location.origin, expectedOrigin: window.location.origin,
// expectedRPID: window.location.hostname, expectedRPID: window.location.hostname,
// response: { response: {
// authenticatorAttachment: "platform", authenticatorAttachment: "platform",
// clientExtensionResults: {}, clientExtensionResults: {},
// id: credId, id: credId,
// rawId: arrayToBase64Url(rawId), rawId: arrayToBase64Url(rawId),
// response: { response: {
// authenticatorData: authData, authenticatorData: authData,
// clientDataJSON: arrayToBase64Url( clientDataJSON: arrayToBase64Url(
// Buffer.from(JSON.stringify(clientDataJSON)), Buffer.from(JSON.stringify(clientDataJSON)),
// ), ),
// signature: signature, signature: signature,
// }, },
// type: "public-key", type: "public-key",
// }, },
// }; };
// console.log("auth opts", authOpts); const verification = await verifyAuthenticationResponse(authOpts);
// const verification = await verifyAuthenticationResponse(authOpts); return verification.verified;
// console.log("auth verification", verification); }
// I'd love to use this but it doesn't verify.
// Pequires:
// npm install @noble/curves
// ... and this import:
// import { p256 } from "@noble/curves/p256";
export async function verifyJwtP256(
jwt: string,
credId: Base64URLString,
rawId: Uint8Array,
authenticatorData: ArrayBuffer,
authenticatorDataBase64Url: Base64URLString,
challenge: Uint8Array,
clientDataJSON: object,
clientDataJsonBase64Url: Base64URLString,
publicKeyBytes: Uint8Array,
publicKeyJwk: JWK,
signature: Base64URLString,
) {
const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64"); const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64");
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
const sigBuffer = Buffer.from(signature, "base64"); const sigBuffer = Buffer.from(signature, "base64");
@@ -487,17 +347,37 @@ export async function verifyJwt(
// Construct the preimage // Construct the preimage
const preimage = Buffer.concat([authDataFromBase, hash]); const preimage = Buffer.concat([authDataFromBase, hash]);
// console.log("finalSigBuffer", finalSigBuffer); const isValid = p256.verify(
// console.log("preimage", preimage); finalSigBuffer,
// console.log("publicKeyBytes", publicKeyBytes); new Uint8Array(preimage),
publicKeyBytes,
);
return isValid;
}
// This uses p256 from @noble/curves/p256, which I would prefer but it's returning false. export async function verifyJwtWebCrypto(
// const isValid = p256.verify( jwt: string,
// finalSigBuffer, credId: Base64URLString,
// new Uint8Array(preimage), rawId: Uint8Array,
// publicKeyBytes, authenticatorData: ArrayBuffer,
// ); authenticatorDataBase64Url: Base64URLString,
// console.log("isValid", isValid); challenge: Uint8Array,
clientDataJSON: object,
clientDataJsonBase64Url: Base64URLString,
publicKeyBytes: Uint8Array,
publicKeyJwk: JWK,
signature: Base64URLString,
) {
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]);
const WebCrypto = await getWebCrypto(); const WebCrypto = await getWebCrypto();
const verifyAlgorithm = { const verifyAlgorithm = {
@@ -515,17 +395,12 @@ export async function verifyJwt(
false, false,
["verify"], ["verify"],
); );
// console.log("verifyAlgorithm", verifyAlgorithm);
// console.log("publicKeyCryptoKey", publicKeyCryptoKey);
// console.log("finalSigBuffer", finalSigBuffer);
// console.log("preimage", preimage);
const verified = await WebCrypto.subtle.verify( const verified = await WebCrypto.subtle.verify(
verifyAlgorithm, verifyAlgorithm,
publicKeyCryptoKey, publicKeyCryptoKey,
finalSigBuffer, finalSigBuffer,
preimage, preimage,
); );
// console.log("verified", verified);
return verified; return verified;
} }
@@ -628,7 +503,6 @@ function base64URLStringToBuffer(base64URLString) {
function cborToKeys(publicKeyBytes: Uint8Array) { function cborToKeys(publicKeyBytes: Uint8Array) {
const jwkObj = cborDecode(publicKeyBytes); const jwkObj = cborDecode(publicKeyBytes);
console.log("jwkObj from verification", jwkObj);
if ( if (
jwkObj[1] != 2 || // kty "EC" jwkObj[1] != 2 || // kty "EC"
jwkObj[3] != -7 || // alg "ES256" jwkObj[3] != -7 || // alg "ES256"
@@ -663,7 +537,7 @@ async function pemToCryptoKey(pem: string) {
for (let i = 0; i < binaryDerString.length; i++) { for (let i = 0; i < binaryDerString.length; i++) {
binaryDer[i] = binaryDerString.charCodeAt(i); binaryDer[i] = binaryDerString.charCodeAt(i);
} }
console.log("binaryDer", binaryDer.buffer); // console.log("binaryDer", binaryDer.buffer);
return await window.crypto.subtle.importKey( return await window.crypto.subtle.importKey(
"spki", "spki",
binaryDer.buffer, binaryDer.buffer,

View File

@@ -172,24 +172,51 @@
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-bold mb-4">Passkeys</h2> <h2 class="text-xl font-bold mb-4">Passkeys</h2>
<button <div>
@click="register()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Register Register
</button> <button
<button @click="register()"
@click="create()" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" >
> Simplewebauthn
</button>
</div>
<div>
Create Create
</button> <button
<button @click="createJwtSimplewebauthn()"
@click="verify()" class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2" >
> Simplewebauthn
</button>
<button
@click="createJwtNavigator()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Navigator
</button>
</div>
<div>
Verify Verify
</button> <button
@click="verifySimplewebauthn()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
Simplewebauthn
</button>
<button
@click="verifyWebCrypto()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
WebCrypto
</button>
<button
@click="verifyP256()"
class="font-bold uppercase bg-slate-600 text-white px-3 py-2 rounded-md mr-2"
>
p256 - broken
</button>
</div>
</div> </div>
</section> </section>
</template> </template>
@@ -206,11 +233,11 @@ import {
createPeerDid, createPeerDid,
JWK, JWK,
PeerSetup, PeerSetup,
registerCredential, registerCredential, verifyJwtP256,
verifyJwt, verifyJwtSimplewebauthn,
verifyJwtWebCrypto,
} from "@/libs/didPeer"; } from "@/libs/didPeer";
import { Buffer } from "buffer"; import { JWTPayload } from "did-jwt";
import {JWTPayload} from "did-jwt";
const inputFileNameRef = ref<Blob>(); const inputFileNameRef = ref<Blob>();
@@ -220,21 +247,12 @@ export default class Help extends Vue {
fileName?: string; fileName?: string;
// for passkeys // for passkeys
authenticatorData?: ArrayBuffer;
credId?: Base64URLString; credId?: Base64URLString;
jwt?: string; jwt?: string;
jwt2?: string;
payload = {
"@context": "https://schema.org",
type: "GiveAction",
description: "pizza",
};
peerSetup?: PeerSetup; peerSetup?: PeerSetup;
peerSetup2?: PeerSetup;
publicKeyJwk?: JWK; publicKeyJwk?: JWK;
publicKeyBytes?: Uint8Array; publicKeyBytes?: Uint8Array;
rawId?: Uint8Array; rawId?: Uint8Array;
savedCredentialId = "";
userId?: ArrayBuffer; userId?: ArrayBuffer;
async uploadFile(event: Event) { async uploadFile(event: Event) {
@@ -269,8 +287,6 @@ export default class Help extends Vue {
public async register() { public async register() {
this.userId = generateRandomBytes(16).buffer; this.userId = generateRandomBytes(16).buffer;
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); console.log("public key", cred);
@@ -278,55 +294,59 @@ export default class Help extends Vue {
this.publicKeyBytes = cred.publicKeyBytes; this.publicKeyBytes = cred.publicKeyBytes;
this.credId = cred.credId as string; this.credId = cred.credId as string;
this.rawId = cred.rawId as Uint8Array; this.rawId = cred.rawId as Uint8Array;
this.savedCredentialId = this.credId;
} }
public async create() { public async createJwtSimplewebauthn() {
console.log("Starting a create");
const did = createPeerDid(this.publicKeyBytes as Uint8Array); const did = createPeerDid(this.publicKeyBytes as Uint8Array);
// console.log("did", did); console.log("generated peer did", did);
const payload = {
"@context": "https://schema.org",
type: "GiveAction",
description: "pizza",
};
// from createJWT in did-jwt/src/JWT.ts // from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = { const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000), iat: Math.floor(Date.now() / 1000),
exp: undefined, exp: undefined,
}; };
const fullPayload = { ...timestamps, ...this.payload, did }; const fullPayload = { ...timestamps, ...payload, did };
this.peerSetup = new PeerSetup(); this.peerSetup = new PeerSetup();
const rawJwt = await this.peerSetup.createJwt( this.jwt = await this.peerSetup.createJwtSimplewebauthn(
fullPayload, fullPayload,
this.credId as string, this.credId as string,
); );
//console.log("simple raw jwt", rawJwt);
this.jwt = rawJwt
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
console.log("simple jwt4url", this.jwt); console.log("simple jwt4url", this.jwt);
// console.log("simple peerSetup", this.peerSetup);
this.peerSetup2 = new PeerSetup();
const rawJwt2 = await this.peerSetup2.createJwt2(
fullPayload,
this.credId as string,
);
// console.log("lower raw jwt2", rawJwt2);
this.jwt2 = rawJwt2
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
console.log("lower jwt4url2", this.jwt2);
// console.log("lower peerSetup2", this.peerSetup2);
} }
public async verify() { public async createJwtNavigator() {
if (!this.jwt || !this.peerSetup) { const did = createPeerDid(this.publicKeyBytes as Uint8Array);
alert("Create a JWT first."); console.log("generated peer did", did);
return;
} const payload = {
const decoded = await verifyJwt( "@context": "https://schema.org",
this.jwt, type: "GiveAction",
description: "pizza",
};
// from createJWT in did-jwt/src/JWT.ts
const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000),
exp: undefined,
};
const fullPayload = { ...timestamps, ...payload, did };
this.peerSetup = new PeerSetup();
this.jwt = await this.peerSetup.createJwtNavigator(
fullPayload,
this.credId as string,
);
console.log("lower jwt4url", this.jwt);
}
public async verifyP256() {
const decoded = await verifyJwtP256(
this.jwt as string,
this.credId as Base64URLString, this.credId as Base64URLString,
this.rawId as Uint8Array, this.rawId as Uint8Array,
this.peerSetup.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
@@ -339,21 +359,40 @@ export default class Help extends Vue {
this.peerSetup.signature as Base64URLString, this.peerSetup.signature as Base64URLString,
); );
console.log("decoded", decoded); console.log("decoded", decoded);
}
const decoded2 = await verifyJwt( public async verifySimplewebauthn() {
this.jwt2 as string, const decoded = await verifyJwtSimplewebauthn(
this.jwt as string,
this.credId as Base64URLString, this.credId as Base64URLString,
this.rawId as Uint8Array, this.rawId as Uint8Array,
this.peerSetup2.authenticatorData as ArrayBuffer, this.peerSetup.authenticatorData as ArrayBuffer,
this.peerSetup2.authenticatorDataBase64Url as Base64URLString, this.peerSetup.authenticatorDataBase64Url as Base64URLString,
this.peerSetup2.challenge as Uint8Array, this.peerSetup.challenge as Uint8Array,
this.peerSetup2.clientDataJsonDecoded, this.peerSetup.clientDataJsonDecoded,
this.peerSetup2.clientDataJsonBase64Url as Base64URLString, this.peerSetup.clientDataJsonBase64Url as Base64URLString,
this.publicKeyBytes as Uint8Array, this.publicKeyBytes as Uint8Array,
this.publicKeyJwk as JWK, this.publicKeyJwk as JWK,
this.peerSetup2.signature as Base64URLString, this.peerSetup.signature as Base64URLString,
); );
console.log("decoded2", decoded2); console.log("decoded", decoded);
}
public async verifyWebCrypto() {
const decoded = await verifyJwtWebCrypto(
this.jwt as string,
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);
} }
} }
</script> </script>