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>
				 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