Browse Source
Co-authored-by: Trent Larson <trent@trentlarson.com> Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/116 Co-authored-by: trentlarson <trent@trentlarson.com> Co-committed-by: trentlarson <trent@trentlarson.com>pull/120/head
trentlarson
5 months ago
11 changed files with 1837 additions and 1475 deletions
File diff suppressed because it is too large
@ -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; |
||||
|
}, |
||||
|
}; |
@ -0,0 +1,570 @@ |
|||||
|
import asn1 from "asn1-ber"; |
||||
|
import { Buffer } from "buffer/"; |
||||
|
import { decode as cborDecode } from "cbor-x"; |
||||
|
import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt"; |
||||
|
import { DIDResolutionResult } from "did-resolver"; |
||||
|
import { sha256 } from "ethereum-cryptography/sha256.js"; |
||||
|
import { |
||||
|
startAuthentication, |
||||
|
startRegistration, |
||||
|
} from "@simplewebauthn/browser"; |
||||
|
import { |
||||
|
generateAuthenticationOptions, |
||||
|
generateRegistrationOptions, |
||||
|
verifyAuthenticationResponse, |
||||
|
verifyRegistrationResponse, |
||||
|
} from "@simplewebauthn/server"; |
||||
|
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; |
||||
|
import { |
||||
|
Base64URLString, |
||||
|
PublicKeyCredentialCreationOptionsJSON, |
||||
|
PublicKeyCredentialRequestOptionsJSON, |
||||
|
} from "@simplewebauthn/types"; |
||||
|
|
||||
|
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; |
||||
|
|
||||
|
const PEER_DID_PREFIX = "did:peer:"; |
||||
|
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; |
||||
|
export interface JWK { |
||||
|
kty: string; |
||||
|
crv: string; |
||||
|
x: string; |
||||
|
y: string; |
||||
|
} |
||||
|
|
||||
|
function toBase64Url(anythingB64: string) { |
||||
|
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); |
||||
|
} |
||||
|
|
||||
|
function arrayToBase64Url(anything: Uint8Array) { |
||||
|
return toBase64Url(Buffer.from(anything).toString("base64")); |
||||
|
} |
||||
|
|
||||
|
export async function registerCredential(passkeyName?: string) { |
||||
|
const options: PublicKeyCredentialCreationOptionsJSON = |
||||
|
await generateRegistrationOptions({ |
||||
|
rpName: "Time Safari", |
||||
|
rpID: window.location.hostname, |
||||
|
userName: passkeyName || "Time Safari User", |
||||
|
// Don't prompt users for additional information about the authenticator
|
||||
|
// (Recommended for smoother UX)
|
||||
|
attestationType: "none", |
||||
|
authenticatorSelection: { |
||||
|
// Defaults
|
||||
|
residentKey: "preferred", |
||||
|
userVerification: "preferred", |
||||
|
// Optional
|
||||
|
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 verification = await verifyRegistrationResponse({ |
||||
|
response: attResp, |
||||
|
expectedChallenge: options.challenge, |
||||
|
expectedOrigin: window.location.origin, |
||||
|
expectedRPID: window.location.hostname, |
||||
|
}); |
||||
|
|
||||
|
// references for parsing auth data and getting the public key
|
||||
|
// https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
|
||||
|
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
||||
|
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
|
||||
|
|
||||
|
const credIdBase64Url = verification.registrationInfo?.credentialID as string; |
||||
|
if (attResp.rawId !== credIdBase64Url) { |
||||
|
console.log("Warning! The raw ID does not match the credential ID.") |
||||
|
} |
||||
|
const credIdHex = Buffer.from( |
||||
|
base64URLStringToArrayBuffer(credIdBase64Url), |
||||
|
).toString("hex"); |
||||
|
const { publicKeyJwk } = cborToKeys( |
||||
|
verification.registrationInfo?.credentialPublicKey as Uint8Array, |
||||
|
); |
||||
|
|
||||
|
return { |
||||
|
authData: verification.registrationInfo?.attestationObject, |
||||
|
credIdHex: credIdHex, |
||||
|
publicKeyJwk: publicKeyJwk, |
||||
|
publicKeyBytes: verification.registrationInfo |
||||
|
?.credentialPublicKey as Uint8Array, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export function createPeerDid(publicKeyBytes: Uint8Array) { |
||||
|
// 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 methodSpecificId = bytesToMultibase( |
||||
|
publicKeyBytes, |
||||
|
"base58btc", |
||||
|
"p256-pub", |
||||
|
); |
||||
|
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId; |
||||
|
} |
||||
|
|
||||
|
function peerDidToPublicKeyBytes(did: string) { |
||||
|
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length)); |
||||
|
} |
||||
|
|
||||
|
export class PeerSetup { |
||||
|
public authenticatorData?: ArrayBuffer; |
||||
|
public challenge?: Uint8Array; |
||||
|
public clientDataJsonBase64Url?: Base64URLString; |
||||
|
public signature?: Base64URLString; |
||||
|
|
||||
|
public async createJwtSimplewebauthn( |
||||
|
issuerDid: string, |
||||
|
payload: object, |
||||
|
credIdHex: string, |
||||
|
) { |
||||
|
const credentialId = arrayBufferToBase64URLString( |
||||
|
Buffer.from(credIdHex, "hex").buffer, |
||||
|
); |
||||
|
const fullPayload = { |
||||
|
...payload, |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
iss: issuerDid, |
||||
|
}; |
||||
|
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); |
||||
|
// const payloadHash: Uint8Array = sha256(this.challenge);
|
||||
|
const options: PublicKeyCredentialRequestOptionsJSON = |
||||
|
await generateAuthenticationOptions({ |
||||
|
challenge: this.challenge, |
||||
|
rpID: window.location.hostname, |
||||
|
allowCredentials: [{ id: credentialId }], |
||||
|
}); |
||||
|
// console.log("simple authentication options", options);
|
||||
|
|
||||
|
const clientAuth = await startAuthentication(options); |
||||
|
// console.log("simple credential get", clientAuth);
|
||||
|
|
||||
|
const authenticatorDataBase64Url = clientAuth.response.authenticatorData; |
||||
|
this.authenticatorData = Buffer.from( |
||||
|
clientAuth.response.authenticatorData, |
||||
|
"base64", |
||||
|
).buffer; |
||||
|
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; |
||||
|
// console.log("simple authenticatorData for signing", this.authenticatorData);
|
||||
|
this.signature = clientAuth.response.signature; |
||||
|
|
||||
|
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||
|
const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; |
||||
|
const headerBase64 = Buffer.from(JSON.stringify(header)) |
||||
|
.toString("base64") |
||||
|
.replace(/\+/g, "-") |
||||
|
.replace(/\//g, "_") |
||||
|
.replace(/=+$/, ""); |
||||
|
|
||||
|
const dataInJwt = { |
||||
|
AuthenticationDataB64URL: authenticatorDataBase64Url, |
||||
|
ClientDataJSONB64URL: this.clientDataJsonBase64Url, |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
iss: issuerDid, |
||||
|
}; |
||||
|
const dataInJwtString = JSON.stringify(dataInJwt); |
||||
|
const payloadBase64 = Buffer.from(dataInJwtString) |
||||
|
.toString("base64") |
||||
|
.replace(/\+/g, "-") |
||||
|
.replace(/\//g, "_") |
||||
|
.replace(/=+$/, ""); |
||||
|
|
||||
|
const signature = clientAuth.response.signature; |
||||
|
|
||||
|
return headerBase64 + "." + payloadBase64 + "." + signature; |
||||
|
} |
||||
|
|
||||
|
public async createJwtNavigator( |
||||
|
issuerDid: string, |
||||
|
payload: object, |
||||
|
credIdHex: string, |
||||
|
) { |
||||
|
const fullPayload = { |
||||
|
...payload, |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
iss: issuerDid, |
||||
|
}; |
||||
|
const dataToSignString = JSON.stringify(fullPayload); |
||||
|
const dataToSignBuffer = Buffer.from(dataToSignString); |
||||
|
const credentialId = Buffer.from(credIdHex, "hex"); |
||||
|
|
||||
|
// console.log("lower credentialId", credentialId);
|
||||
|
this.challenge = new Uint8Array(dataToSignBuffer); |
||||
|
const options = { |
||||
|
publicKey: { |
||||
|
allowCredentials: [ |
||||
|
{ |
||||
|
id: credentialId, |
||||
|
type: "public-key", |
||||
|
}, |
||||
|
], |
||||
|
challenge: this.challenge.buffer, |
||||
|
rpID: window.location.hostname, |
||||
|
userVerification: "preferred", |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
const credential = await navigator.credentials.get(options); |
||||
|
// console.log("nav credential get", credential);
|
||||
|
|
||||
|
this.authenticatorData = credential?.response.authenticatorData; |
||||
|
const authenticatorDataBase64Url = arrayBufferToBase64URLString( |
||||
|
this.authenticatorData, |
||||
|
); |
||||
|
|
||||
|
this.clientDataJsonBase64Url = arrayBufferToBase64URLString( |
||||
|
credential?.response.clientDataJSON, |
||||
|
); |
||||
|
|
||||
|
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
||||
|
const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; |
||||
|
const headerBase64 = Buffer.from(JSON.stringify(header)) |
||||
|
.toString("base64") |
||||
|
.replace(/\+/g, "-") |
||||
|
.replace(/\//g, "_") |
||||
|
.replace(/=+$/, ""); |
||||
|
|
||||
|
const dataInJwt = { |
||||
|
AuthenticationDataB64URL: authenticatorDataBase64Url, |
||||
|
ClientDataJSONB64URL: this.clientDataJsonBase64Url, |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
iss: issuerDid, |
||||
|
}; |
||||
|
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;
|
||||
|
// };
|
||||
|
// }
|
||||
|
} |
||||
|
|
||||
|
// I'd love to use this but it doesn't verify.
|
||||
|
// Requires:
|
||||
|
// npm install @noble/curves
|
||||
|
// ... and this import:
|
||||
|
// import { p256 } from "@noble/curves/p256";
|
||||
|
export async function verifyJwtP256( |
||||
|
credIdHex: string, |
||||
|
issuerDid: string, |
||||
|
authenticatorData: ArrayBuffer, |
||||
|
challenge: Uint8Array, |
||||
|
clientDataJsonBase64Url: Base64URLString, |
||||
|
signature: Base64URLString, |
||||
|
) { |
||||
|
const authDataFromBase = Buffer.from(authenticatorData); |
||||
|
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); |
||||
|
const sigBuffer = Buffer.from(signature, "base64"); |
||||
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer); |
||||
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
||||
|
|
||||
|
// Hash the client data
|
||||
|
const hash = sha256(clientDataFromBase); |
||||
|
|
||||
|
// Construct the preimage
|
||||
|
const preimage = Buffer.concat([authDataFromBase, hash]); |
||||
|
|
||||
|
const isValid = p256.verify( |
||||
|
finalSigBuffer, |
||||
|
new Uint8Array(preimage), |
||||
|
publicKeyBytes, |
||||
|
); |
||||
|
return isValid; |
||||
|
} |
||||
|
|
||||
|
export async function verifyJwtSimplewebauthn( |
||||
|
credIdHex: string, |
||||
|
issuerDid: string, |
||||
|
authenticatorData: ArrayBuffer, |
||||
|
challenge: Uint8Array, |
||||
|
clientDataJsonBase64Url: Base64URLString, |
||||
|
signature: Base64URLString, |
||||
|
) { |
||||
|
const authData = arrayToBase64Url(Buffer.from(authenticatorData)); |
||||
|
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
||||
|
const credId = arrayBufferToBase64URLString( |
||||
|
Buffer.from(credIdHex, "hex").buffer, |
||||
|
); |
||||
|
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: credId, |
||||
|
response: { |
||||
|
authenticatorData: authData, |
||||
|
clientDataJSON: clientDataJsonBase64Url, |
||||
|
signature: signature, |
||||
|
}, |
||||
|
type: "public-key", |
||||
|
}, |
||||
|
}; |
||||
|
const verification = await verifyAuthenticationResponse(authOpts); |
||||
|
return verification.verified; |
||||
|
} |
||||
|
|
||||
|
export async function verifyJwtWebCrypto( |
||||
|
credId: Base64URLString, |
||||
|
issuerDid: string, |
||||
|
authenticatorData: ArrayBuffer, |
||||
|
challenge: Uint8Array, |
||||
|
clientDataJsonBase64Url: Base64URLString, |
||||
|
signature: Base64URLString, |
||||
|
) { |
||||
|
const authDataFromBase = Buffer.from(authenticatorData); |
||||
|
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 publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
||||
|
|
||||
|
const WebCrypto = await getWebCrypto(); |
||||
|
const verifyAlgorithm = { |
||||
|
name: "ECDSA", |
||||
|
hash: { name: "SHA-256" }, |
||||
|
}; |
||||
|
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; |
||||
|
const keyAlgorithm = { |
||||
|
name: "ECDSA", |
||||
|
namedCurve: publicKeyJwk.crv, |
||||
|
}; |
||||
|
const publicKeyCryptoKey = await WebCrypto.subtle.importKey( |
||||
|
"jwk", |
||||
|
publicKeyJwk, |
||||
|
keyAlgorithm, |
||||
|
false, |
||||
|
["verify"], |
||||
|
); |
||||
|
const verified = await WebCrypto.subtle.verify( |
||||
|
verifyAlgorithm, |
||||
|
publicKeyCryptoKey, |
||||
|
finalSigBuffer, |
||||
|
preimage, |
||||
|
); |
||||
|
return verified; |
||||
|
} |
||||
|
|
||||
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
||||
|
if (!did.startsWith("did:peer:0z")) { |
||||
|
throw new Error( |
||||
|
"This only verifies a peer DID, method 0, encoded base58btc.", |
||||
|
); |
||||
|
} |
||||
|
// this is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
||||
|
// (another reference is the @aviarytech/did-peer resolver)
|
||||
|
const id = did.split(":")[2]; |
||||
|
const multibase = id.slice(1); |
||||
|
const encnumbasis = multibase.slice(1); |
||||
|
const didDocument = { |
||||
|
"@context": [ |
||||
|
"https://www.w3.org/ns/did/v1", |
||||
|
"https://w3id.org/security/suites/jws-2020/v1", |
||||
|
], |
||||
|
assertionMethod: [did + "#" + encnumbasis], |
||||
|
authentication: [did + "#" + encnumbasis], |
||||
|
capabilityDelegation: [did + "#" + encnumbasis], |
||||
|
capabilityInvocation: [did + "#" + encnumbasis], |
||||
|
id: did, |
||||
|
keyAgreement: undefined, |
||||
|
service: undefined, |
||||
|
verificationMethod: [ |
||||
|
{ |
||||
|
controller: did, |
||||
|
id: did + "#" + encnumbasis, |
||||
|
publicKeyMultibase: multibase, |
||||
|
type: "EcdsaSecp256k1VerificationKey2019", |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
return { |
||||
|
didDocument, |
||||
|
didDocumentMetadata: {}, |
||||
|
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(/=+$/, ""); |
||||
|
} |
||||
|
|
||||
|
// from @simplewebauthn/browser
|
||||
|
function arrayBufferToBase64URLString(buffer) { |
||||
|
const bytes = new Uint8Array(buffer); |
||||
|
let str = ""; |
||||
|
for (const charCode of bytes) { |
||||
|
str += String.fromCharCode(charCode); |
||||
|
} |
||||
|
const base64String = btoa(str); |
||||
|
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); |
||||
|
} |
||||
|
|
||||
|
// from @simplewebauthn/browser
|
||||
|
function base64URLStringToArrayBuffer(base64URLString: string) { |
||||
|
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/"); |
||||
|
const padLength = (4 - (base64.length % 4)) % 4; |
||||
|
const padded = base64.padEnd(base64.length + padLength, "="); |
||||
|
const binary = atob(padded); |
||||
|
const buffer = new ArrayBuffer(binary.length); |
||||
|
const bytes = new Uint8Array(buffer); |
||||
|
for (let i = 0; i < binary.length; i++) { |
||||
|
bytes[i] = binary.charCodeAt(i); |
||||
|
} |
||||
|
return buffer; |
||||
|
} |
||||
|
|
||||
|
function cborToKeys(publicKeyBytes: Uint8Array) { |
||||
|
const jwkObj = cborDecode(publicKeyBytes); |
||||
|
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"], |
||||
|
); |
||||
|
} |
Loading…
Reference in new issue