import { Buffer } from 'buffer/' import { JWTPayload } 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 { AppString } from '../../../constants/app' import { unwrapEC2Signature } from '../../../libs/crypto/vc/passkeyHelpers' import { arrayToBase64Url, cborToKeys, peerDidToPublicKeyBytes, verifyPeerSignature } from '../../../libs/crypto/vc/didPeer' import { logger } from '../../../utils/logger' export interface JWK { kty: string crv: string x: string y: string } export async function registerCredential(passkeyName?: string) { const options: PublicKeyCredentialCreationOptionsJSON = await generateRegistrationOptions({ rpName: AppString.APP_NAME, rpID: window.location.hostname, userName: passkeyName || AppString.APP_NAME + ' 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) { logger.warn('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 class PeerSetup { public authenticatorData?: ArrayBuffer public challenge?: Uint8Array public clientDataJsonBase64Url?: Base64URLString public signature?: Base64URLString public async createJwtSimplewebauthn( issuerDid: string, payload: object, credIdHex: string, expMinutes: number = 1 ) { const credentialId = arrayBufferToBase64URLString( Buffer.from(credIdHex, 'hex').buffer ) const issuedAt = Math.floor(Date.now() / 1000) const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now const fullPayload = { ...payload, exp: expiryTime, iat: issuedAt, 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, exp: expiryTime, iat: issuedAt, 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, expMinutes: number = 1 ) { const issuedAt = Math.floor(Date.now() / 1000) const expiryTime = Math.floor(Date.now() / 1000) + expMinutes * 60 // some minutes from now const fullPayload = { ...payload, exp: expiryTime, iat: issuedAt, 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' as const } ], challenge: this.challenge.buffer, rpID: window.location.hostname, userVerification: 'preferred' as const } } const credential = await navigator.credentials.get(options) // console.log("nav credential get", credential); this.authenticatorData = credential?.response.authenticatorData const authenticatorDataBase64Url = arrayBufferToBase64URLString( this.authenticatorData as ArrayBuffer ) 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, exp: expiryTime, iat: issuedAt, 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 } // To use this, add the asn1-ber library and add this import: // import asn1 from "asn1-ber"; // // 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 createDidPeerJwt( did: string, credIdHex: string, payload: object ): Promise { const peerSetup = new PeerSetup() const jwt = await peerSetup.createJwtNavigator(did, payload, credIdHex) return jwt } // 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 } // similar code is in endorser-ch util-crypto.ts verifyPeerSignature 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]) return verifyPeerSignature(preimage, issuerDid, finalSigBuffer) } // eslint-disable-next-line @typescript-eslint/no-unused-vars async function peerDidToDidDocument(did: string): Promise { 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) /** * Looks like JsonWebKey2020 isn't too difficult: * - change context security/suites link to jws-2020/v1 * - change publicKeyMultibase to publicKeyJwk generated with cborToKeys * - change type to JsonWebKey2020 */ 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/secp256k1-2019/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 // eslint-disable-next-line @typescript-eslint/no-unused-vars 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 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error because it complains about the type of x and y 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 } // tried the base64url library but got an error using their Buffer export function base64urlDecodeString(input: string) { return atob(input.replace(/-/g, '+').replace(/_/g, '/')) } // tried the base64url library but got an error using their Buffer export function base64urlEncodeString(input: string) { return btoa(input).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') } // eslint-disable-next-line @typescript-eslint/no-unused-vars function base64urlDecodeArrayBuffer(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 } // eslint-disable-next-line @typescript-eslint/no-unused-vars function base64urlEncodeArrayBuffer(buffer: ArrayBuffer) { const str = String.fromCharCode(...new Uint8Array(buffer)) return base64urlEncodeString(str) } // from @simplewebauthn/browser function arrayBufferToBase64URLString(buffer: ArrayBuffer) { 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 } // eslint-disable-next-line @typescript-eslint/no-unused-vars 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'] ) }