Compare commits
	
		
			18 Commits 
		
	
	
	| Author | SHA1 | Date | 
|---|---|---|
|  | d4bf045049 | 1 year ago | 
|  | 8a64da2b5f | 1 year ago | 
|  | cf1137737a | 1 year ago | 
|  | 82f51b6f93 | 1 year ago | 
|  | 83722e0057 | 1 year ago | 
|  | 3bb2498e28 | 1 year ago | 
|  | 80f05ba9e9 | 1 year ago | 
|  | bb555cd6ee | 1 year ago | 
|  | 1dd7c6e3b1 | 1 year ago | 
|  | e8423b1a00 | 1 year ago | 
|  | b4a521c6d4 | 1 year ago | 
|  | a64c7c2848 | 1 year ago | 
|  | 37907ee3ad | 1 year ago | 
|  | 43da8586e5 | 1 year ago | 
|  | 94443c93bc | 1 year ago | 
|  | 2a675eca6a | 1 year ago | 
|  | 94fb76cfdc | 1 year ago | 
|  | 7dde4d4d30 | 1 year 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