|
@ -1,8 +1,8 @@ |
|
|
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, verifyJWT } from "did-jwt"; |
|
|
import { createJWS, JWTPayload } from "did-jwt"; |
|
|
import { DIDResolutionResult, Resolver } 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 { |
|
@ -24,6 +24,7 @@ import { |
|
|
} from "@simplewebauthn/types"; |
|
|
} from "@simplewebauthn/types"; |
|
|
|
|
|
|
|
|
import { generateRandomBytes } from "@/libs/crypto"; |
|
|
import { generateRandomBytes } from "@/libs/crypto"; |
|
|
|
|
|
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; |
|
|
|
|
|
|
|
|
export interface JWK { |
|
|
export interface JWK { |
|
|
kty: string; |
|
|
kty: string; |
|
@ -77,12 +78,24 @@ export async function registerCredential(userId: Uint8Array) { |
|
|
expectedRPID: window.location.hostname, |
|
|
expectedRPID: window.location.hostname, |
|
|
}); |
|
|
}); |
|
|
console.log("verification", verification); |
|
|
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 { |
|
|
return { |
|
|
authData: verification.registrationInfo?.attestationObject, |
|
|
authData: verification.registrationInfo?.attestationObject, |
|
|
credId: verification.registrationInfo?.credentialID as string, |
|
|
credId: verification.registrationInfo?.credentialID as string, |
|
|
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")), |
|
|
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")), |
|
|
publicKeyJwk: undefined, |
|
|
publicKeyJwk: publicKeyJwk, |
|
|
publicKeyBytes: verification.registrationInfo |
|
|
publicKeyBytes: verification.registrationInfo |
|
|
?.credentialPublicKey as Uint8Array, |
|
|
?.credentialPublicKey as Uint8Array, |
|
|
}; |
|
|
}; |
|
@ -153,30 +166,9 @@ export async function registerCredential2(userId: Uint8Array) { |
|
|
); |
|
|
); |
|
|
console.log("attestationObject", attestationObject); |
|
|
console.log("attestationObject", attestationObject); |
|
|
|
|
|
|
|
|
const jwkObj = cborDecode( |
|
|
const { publicKeyJwk, publicKeyBuffer } = cborToKeys( |
|
|
verification.registrationInfo?.credentialPublicKey as Uint8Array, |
|
|
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 publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
|
|
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
|
|
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
|
@ -186,7 +178,7 @@ export async function registerCredential2(userId: Uint8Array) { |
|
|
credId: credential?.id, |
|
|
credId: credential?.id, |
|
|
rawId: credential?.rawId, |
|
|
rawId: credential?.rawId, |
|
|
publicKeyJwk, |
|
|
publicKeyJwk, |
|
|
publicKeyBytes, |
|
|
publicKeyBytes: publicKeyBuffer, |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
@ -208,9 +200,12 @@ export function createPeerDid(publicKeyBytes: Uint8Array) { |
|
|
|
|
|
|
|
|
export class PeerSetup { |
|
|
export class PeerSetup { |
|
|
public authenticatorData?: ArrayBuffer; |
|
|
public authenticatorData?: ArrayBuffer; |
|
|
|
|
|
public authenticatorDataBase64Url?: Base64URLString; |
|
|
public challenge?: Uint8Array; |
|
|
public challenge?: Uint8Array; |
|
|
public clientDataJsonDecoded?: object; |
|
|
public clientDataJsonDecoded?: object; |
|
|
|
|
|
public clientDataJsonBase64Url?: Base64URLString; |
|
|
public signature?: Base64URLString; |
|
|
public signature?: Base64URLString; |
|
|
|
|
|
public publicKeyJwk?: JWK; |
|
|
|
|
|
|
|
|
public async createJwt( |
|
|
public async createJwt( |
|
|
payload: object, |
|
|
payload: object, |
|
@ -244,16 +239,23 @@ export class PeerSetup { |
|
|
const clientAuth = await startAuthentication(options); |
|
|
const clientAuth = await startAuthentication(options); |
|
|
console.log("custom clientAuth", clientAuth); |
|
|
console.log("custom clientAuth", clientAuth); |
|
|
|
|
|
|
|
|
|
|
|
this.authenticatorDataBase64Url = clientAuth.response.authenticatorData; |
|
|
this.authenticatorData = Buffer.from( |
|
|
this.authenticatorData = Buffer.from( |
|
|
clientAuth.response.authenticatorData, |
|
|
clientAuth.response.authenticatorData, |
|
|
"base64", |
|
|
"base64", |
|
|
).buffer; |
|
|
).buffer; |
|
|
this.challenge = payloadHash; |
|
|
this.challenge = payloadHash; |
|
|
|
|
|
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; |
|
|
this.clientDataJsonDecoded = JSON.parse( |
|
|
this.clientDataJsonDecoded = JSON.parse( |
|
|
Buffer.from(clientAuth.response.clientDataJSON, "base64").toString( |
|
|
Buffer.from(clientAuth.response.clientDataJSON, "base64").toString( |
|
|
"utf-8", |
|
|
"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; |
|
|
this.signature = clientAuth.response.signature; |
|
|
|
|
|
|
|
|
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); |
|
|
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64"); |
|
@ -373,9 +375,12 @@ export async function verifyJwt( |
|
|
credId: Base64URLString, |
|
|
credId: Base64URLString, |
|
|
rawId: Uint8Array, |
|
|
rawId: Uint8Array, |
|
|
authenticatorData: ArrayBuffer, |
|
|
authenticatorData: ArrayBuffer, |
|
|
|
|
|
authenticatorDataBase64Url: Base64URLString, |
|
|
challenge: Uint8Array, |
|
|
challenge: Uint8Array, |
|
|
clientDataJSON: object, |
|
|
clientDataJSON: object, |
|
|
publicKey: Uint8Array, |
|
|
clientDataJsonBase64Url: Base64URLString, |
|
|
|
|
|
publicKeyBytes: Uint8Array, |
|
|
|
|
|
publicKeyJwk: JWK, |
|
|
signature: Base64URLString, |
|
|
signature: Base64URLString, |
|
|
) { |
|
|
) { |
|
|
// Here's a combined auth & verify process, based on some of the inputs.
|
|
|
// Here's a combined auth & verify process, based on some of the inputs.
|
|
@ -397,7 +402,7 @@ export async function verifyJwt( |
|
|
// const verfOpts: VerifyAuthenticationResponseOpts = {
|
|
|
// const verfOpts: VerifyAuthenticationResponseOpts = {
|
|
|
// authenticator: {
|
|
|
// authenticator: {
|
|
|
// credentialID: credId,
|
|
|
// credentialID: credId,
|
|
|
// credentialPublicKey: publicKey,
|
|
|
// credentialPublicKey: publicKeyBytes,
|
|
|
// counter: 0,
|
|
|
// counter: 0,
|
|
|
// },
|
|
|
// },
|
|
|
// expectedChallenge: options.challenge,
|
|
|
// expectedChallenge: options.challenge,
|
|
@ -413,7 +418,7 @@ export async function verifyJwt( |
|
|
const authOpts: VerifyAuthenticationResponseOpts = { |
|
|
const authOpts: VerifyAuthenticationResponseOpts = { |
|
|
authenticator: { |
|
|
authenticator: { |
|
|
credentialID: credId, |
|
|
credentialID: credId, |
|
|
credentialPublicKey: publicKey, |
|
|
credentialPublicKey: publicKeyBytes, |
|
|
counter: 0, |
|
|
counter: 0, |
|
|
}, |
|
|
}, |
|
|
expectedChallenge: arrayToBase64Url(challenge), |
|
|
expectedChallenge: arrayToBase64Url(challenge), |
|
@ -438,10 +443,107 @@ export async function verifyJwt( |
|
|
const verification = await verifyAuthenticationResponse(authOpts); |
|
|
const verification = await verifyAuthenticationResponse(authOpts); |
|
|
console.log("auth verification", verification); |
|
|
console.log("auth verification", verification); |
|
|
|
|
|
|
|
|
const decoded = verifyJWT(jwt, { |
|
|
// It doesn't work to use the did-jwt verifyJWT with did-resolver Resolver
|
|
|
resolver: new Resolver({ peer: peerDidToDidDocument }), |
|
|
// const decoded = verifyJWT(jwt, {
|
|
|
}); |
|
|
// resolver: new Resolver({ peer: peerDidToDidDocument }),
|
|
|
return decoded; |
|
|
// });
|
|
|
|
|
|
// 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> { |
|
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
|
@ -481,3 +583,86 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
|
|
didResolutionMetadata: { contentType: "application/did+ld+json" }, |
|
|
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"], |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|