|
@ -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,65 @@ 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 headerBase64 = Buffer.from(JSON.stringify(header)) |
|
|
|
|
|
.toString("base64") |
|
|
|
|
|
.replace(/\+/g, "-") |
|
|
|
|
|
.replace(/\//g, "_") |
|
|
|
|
|
.replace(/=+$/, ""); |
|
|
|
|
|
|
|
|
|
|
|
const dataInJwt = { |
|
|
|
|
|
AuthenticationData: this.authenticatorDataBase64Url, |
|
|
|
|
|
ClientDataJSON: this.clientDataJsonBase64Url, |
|
|
|
|
|
}; |
|
|
|
|
|
const dataInJwtString = JSON.stringify(dataInJwt); |
|
|
|
|
|
const payloadBase64 = Buffer.from(dataInJwtString).toString("base64"); |
|
|
|
|
|
|
|
|
const signature = clientAuth.response.signature; |
|
|
const signature = clientAuth.response.signature; |
|
|
|
|
|
|
|
|
return headerBase64 + "." + payloadBase64 + "." + signature; |
|
|
return headerBase64 + "." + payloadBase64 + "." + signature; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
public async createJwt2(fullPayload: object, credentialId: string) { |
|
|
public async createJwtNavigator(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 dataToSignString = JSON.stringify(fullPayload); |
|
|
const dataToSignBuffer = Buffer.from(dataToSignString); |
|
|
const dataToSignBuffer = Buffer.from(dataToSignString); |
|
|
|
|
|
|
|
|
//console.log("lower credentialId", credentialId);
|
|
|
// console.log("lower credentialId", credentialId);
|
|
|
this.challenge = new Uint8Array(dataToSignBuffer); |
|
|
this.challenge = new Uint8Array(dataToSignBuffer); |
|
|
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);
|
|
|
|
|
|
// 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); |
|
|
const credential = await navigator.credentials.get(options); |
|
|
// console.log("lower credential get", credential);
|
|
|
// console.log("nav credential get", credential);
|
|
|
// console.log("lower credential get in base64", {
|
|
|
|
|
|
// id: credential?.id,
|
|
|
this.authenticatorDataBase64Url = bufferToBase64URLString( |
|
|
// rawId: bufferToBase64URLString(credential?.rawId),
|
|
|
credential?.response.authenticatorData, |
|
|
// 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.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( |
|
|
this.clientDataJsonDecoded = JSON.parse( |
|
|
new TextDecoder("utf-8").decode( |
|
|
new TextDecoder("utf-8").decode(credential?.response.clientDataJSON), |
|
|
authenticatorAssertionResponse.clientDataJSON, |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
); |
|
|
// console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded);
|
|
|
|
|
|
|
|
|
|
|
|
const origSignature = 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)) |
|
|
this.signature = origSignature |
|
|
.toString("base64") |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/=+$/, ""); |
|
|
.replace(/=+$/, ""); |
|
@ -319,122 +203,159 @@ export class PeerSetup { |
|
|
ClientDataJSON: this.clientDataJsonBase64Url, |
|
|
ClientDataJSON: this.clientDataJsonBase64Url, |
|
|
}; |
|
|
}; |
|
|
const dataInJwtString = JSON.stringify(dataInJwt); |
|
|
const dataInJwtString = JSON.stringify(dataInJwt); |
|
|
const payloadBase64 = Buffer.from(dataInJwtString).toString("base64"); |
|
|
const payloadBase64 = Buffer.from(dataInJwtString) |
|
|
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature; |
|
|
.toString("base64") |
|
|
return jwt; |
|
|
.replace(/\+/g, "-") |
|
|
} |
|
|
.replace(/\//g, "_") |
|
|
|
|
|
.replace(/=+$/, ""); |
|
|
// Attempted with JWS, but it will not match because it signs different content (header + payload)
|
|
|
|
|
|
//const signer = await this.webAuthnES256KSigner(credentialId);
|
|
|
|
|
|
//const jwt = createJWS(fullPayload, signer, { typ: "JWT", alg: "ES256" });
|
|
|
|
|
|
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.
|
|
|
|
|
|
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"); |
|
|
const origSignature = Buffer.from(credential?.response.signature) |
|
|
console.log("lower combSig64", combSig64); |
|
|
.toString("base64") |
|
|
const combSig64Url = combSig64 |
|
|
this.signature = origSignature |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\+/g, "-") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/\//g, "_") |
|
|
.replace(/=+$/, ""); |
|
|
.replace(/=+$/, ""); |
|
|
console.log("lower combSig64Url", combSig64Url); |
|
|
|
|
|
return combSig64Url; |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async generateWebAuthnSignature( |
|
|
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature; |
|
|
dataToSign: string | Uint8Array, // from Signer interface
|
|
|
return jwt; |
|
|
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; |
|
|
// return a low-level signing function, similar to createJWS approach
|
|
|
const options = { |
|
|
// async webAuthnES256KSigner(credentialID: string) {
|
|
|
publicKey: { |
|
|
// return async (data: string | Uint8Array) => {
|
|
|
challenge: this.challenge.buffer, |
|
|
// // get signature from WebAuthn
|
|
|
rpID: window.location.hostname, |
|
|
// const signature = await this.generateWebAuthnSignature(data);
|
|
|
//allowCredentials: [{ id: credentialId, type: "public-key" }],
|
|
|
//
|
|
|
userVerification: "preferred", |
|
|
// // This converts from the browser ArrayBuffer to a Node.js Buffer, which is a requirement for the asn1 library.
|
|
|
//extensions: fullPayload,
|
|
|
// 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 verifyJwtSimplewebauthn( |
|
|
|
|
|
jwt: string, |
|
|
|
|
|
credId: Base64URLString, |
|
|
|
|
|
rawId: Uint8Array, |
|
|
|
|
|
authenticatorData: ArrayBuffer, |
|
|
|
|
|
authenticatorDataBase64Url: Base64URLString, |
|
|
|
|
|
challenge: Uint8Array, |
|
|
|
|
|
clientDataJSON: object, |
|
|
|
|
|
clientDataJsonBase64Url: Base64URLString, |
|
|
|
|
|
publicKeyBytes: Uint8Array, |
|
|
|
|
|
publicKeyJwk: JWK, |
|
|
|
|
|
signature: Base64URLString, |
|
|
|
|
|
) { |
|
|
|
|
|
const authData = arrayToBase64Url(Buffer.from(authenticatorData)); |
|
|
|
|
|
const authOpts: VerifyAuthenticationResponseOpts = { |
|
|
|
|
|
authenticator: { |
|
|
|
|
|
credentialID: credId, |
|
|
|
|
|
credentialPublicKey: publicKeyBytes, |
|
|
|
|
|
counter: 0, |
|
|
|
|
|
}, |
|
|
|
|
|
expectedChallenge: arrayToBase64Url(challenge), |
|
|
|
|
|
expectedOrigin: window.location.origin, |
|
|
|
|
|
expectedRPID: window.location.hostname, |
|
|
|
|
|
response: { |
|
|
|
|
|
authenticatorAttachment: "platform", |
|
|
|
|
|
clientExtensionResults: {}, |
|
|
|
|
|
id: credId, |
|
|
|
|
|
rawId: arrayToBase64Url(rawId), |
|
|
|
|
|
response: { |
|
|
|
|
|
authenticatorData: authData, |
|
|
|
|
|
clientDataJSON: arrayToBase64Url( |
|
|
|
|
|
Buffer.from(JSON.stringify(clientDataJSON)), |
|
|
|
|
|
), |
|
|
|
|
|
signature: signature, |
|
|
|
|
|
}, |
|
|
|
|
|
type: "public-key", |
|
|
}, |
|
|
}, |
|
|
}; |
|
|
}; |
|
|
|
|
|
const verification = await verifyAuthenticationResponse(authOpts); |
|
|
|
|
|
return verification.verified; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// console.log("lower authentication options", options);
|
|
|
// I'd love to use this but it doesn't verify.
|
|
|
const assertion = await navigator.credentials.get(options); |
|
|
// Pequires:
|
|
|
// console.log("lower credential get", assertion);
|
|
|
// 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 clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); |
|
|
|
|
|
const sigBuffer = Buffer.from(signature, "base64"); |
|
|
|
|
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer); |
|
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
// Hash the client data
|
|
|
|
|
|
const hash = sha256(clientDataFromBase); |
|
|
|
|
|
|
|
|
this.authenticatorDataBase64Url = |
|
|
// Construct the preimage
|
|
|
authenticatorAssertionResponse.authenticatorData; |
|
|
const preimage = Buffer.concat([authDataFromBase, hash]); |
|
|
this.authenticatorData = Buffer.from( |
|
|
|
|
|
this.authenticatorDataBase64Url as Base64URLString, |
|
|
|
|
|
"base64", |
|
|
|
|
|
).buffer; |
|
|
|
|
|
// console.log("lower authenticator data", this.authenticatorData);
|
|
|
|
|
|
|
|
|
|
|
|
this.clientDataJsonBase64Url = |
|
|
const isValid = p256.verify( |
|
|
authenticatorAssertionResponse.clientDataJSON; |
|
|
finalSigBuffer, |
|
|
this.clientDataJsonDecoded = JSON.parse( |
|
|
new Uint8Array(preimage), |
|
|
new TextDecoder("utf-8").decode( |
|
|
publicKeyBytes, |
|
|
authenticatorAssertionResponse.clientDataJSON, |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
); |
|
|
// console.log("lower clientDataJSON decoded", this.clientDataJsonDecoded);
|
|
|
return isValid; |
|
|
|
|
|
|
|
|
this.signature = Buffer.from( |
|
|
|
|
|
authenticatorAssertionResponse.signature, |
|
|
|
|
|
).toString("base64"); |
|
|
|
|
|
|
|
|
|
|
|
return this.signature; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export async function verifyJwt( |
|
|
export async function verifyJwtWebCrypto( |
|
|
jwt: string, |
|
|
jwt: string, |
|
|
credId: Base64URLString, |
|
|
credId: Base64URLString, |
|
|
rawId: Uint8Array, |
|
|
rawId: Uint8Array, |
|
@ -447,35 +368,6 @@ export async function verifyJwt( |
|
|
publicKeyJwk: JWK, |
|
|
publicKeyJwk: JWK, |
|
|
signature: Base64URLString, |
|
|
signature: Base64URLString, |
|
|
) { |
|
|
) { |
|
|
// const authData = arrayToBase64Url(Buffer.from(authenticatorData));
|
|
|
|
|
|
// const authOpts: VerifyAuthenticationResponseOpts = {
|
|
|
|
|
|
// authenticator: {
|
|
|
|
|
|
// credentialID: credId,
|
|
|
|
|
|
// credentialPublicKey: publicKeyBytes,
|
|
|
|
|
|
// counter: 0,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// expectedChallenge: arrayToBase64Url(challenge),
|
|
|
|
|
|
// expectedOrigin: window.location.origin,
|
|
|
|
|
|
// expectedRPID: window.location.hostname,
|
|
|
|
|
|
// response: {
|
|
|
|
|
|
// authenticatorAttachment: "platform",
|
|
|
|
|
|
// clientExtensionResults: {},
|
|
|
|
|
|
// id: credId,
|
|
|
|
|
|
// rawId: arrayToBase64Url(rawId),
|
|
|
|
|
|
// response: {
|
|
|
|
|
|
// authenticatorData: authData,
|
|
|
|
|
|
// clientDataJSON: arrayToBase64Url(
|
|
|
|
|
|
// Buffer.from(JSON.stringify(clientDataJSON)),
|
|
|
|
|
|
// ),
|
|
|
|
|
|
// signature: signature,
|
|
|
|
|
|
// },
|
|
|
|
|
|
// type: "public-key",
|
|
|
|
|
|
// },
|
|
|
|
|
|
// };
|
|
|
|
|
|
// console.log("auth opts", authOpts);
|
|
|
|
|
|
// const verification = await verifyAuthenticationResponse(authOpts);
|
|
|
|
|
|
// console.log("auth verification", verification);
|
|
|
|
|
|
|
|
|
|
|
|
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,18 +379,6 @@ 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);
|
|
|
|
|
|
// 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 WebCrypto = await getWebCrypto(); |
|
|
const verifyAlgorithm = { |
|
|
const verifyAlgorithm = { |
|
|
name: "ECDSA", |
|
|
name: "ECDSA", |
|
@ -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, |
|
|