Browse Source
			
			
			
			
				
		Co-authored-by: Trent Larson <trent@trentlarson.com> Reviewed-on: https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/pulls/120 Co-authored-by: trentlarson <trent@trentlarson.com> Co-committed-by: trentlarson <trent@trentlarson.com>
				 12 changed files with 287 additions and 176 deletions
			
			
		| @ -0,0 +1,96 @@ | |||||
|  | import {Buffer} from "buffer/"; | ||||
|  | import {decode as cborDecode} from "cbor-x"; | ||||
|  | import {bytesToMultibase, multibaseToBytes} from "did-jwt"; | ||||
|  | 
 | ||||
|  | import {getWebCrypto} from "@/libs/crypto/vc/passkeyHelpers"; | ||||
|  | 
 | ||||
|  | const PEER_DID_PREFIX = "did:peer:"; | ||||
|  | const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  * | ||||
|  |  * | ||||
|  |  * similar code is in crowd-funder-for-time-pwa libs/crypto/vc/passkeyDidPeer.ts verifyJwtWebCrypto | ||||
|  |  * | ||||
|  |  * @returns {Promise<boolean>} | ||||
|  |  */ | ||||
|  | export async function verifyPeerSignature( | ||||
|  |   payloadBytes: Buffer, | ||||
|  |   issuerDid: string, | ||||
|  |   signatureBytes: Uint8Array, | ||||
|  | ): Promise<boolean> { | ||||
|  |   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, | ||||
|  |     signatureBytes, | ||||
|  |     payloadBytes, | ||||
|  |   ); | ||||
|  |   return verified; | ||||
|  | } | ||||
|  | 
 | ||||
|  | export 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 }; | ||||
|  | } | ||||
|  | 
 | ||||
|  | export function toBase64Url(anythingB64: string) { | ||||
|  |   return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); | ||||
|  | } | ||||
|  | 
 | ||||
|  | export function arrayToBase64Url(anything: Uint8Array) { | ||||
|  |   return toBase64Url(Buffer.from(anything).toString("base64")); | ||||
|  | } | ||||
|  | 
 | ||||
|  | export function peerDidToPublicKeyBytes(did: string) { | ||||
|  |   return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length)); | ||||
|  | } | ||||
|  | 
 | ||||
|  | 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; | ||||
|  | } | ||||
| @ -0,0 +1,110 @@ | |||||
|  | /** | ||||
|  |  * Verifiable Credential & DID functions, specifically for EndorserSearch.org tools | ||||
|  |  * | ||||
|  |  * The goal is to make this folder similar across projects, then move it to a library. | ||||
|  |  * Other projects: endorser-ch, image-api | ||||
|  |  * | ||||
|  |  */ | ||||
|  | 
 | ||||
|  | import * as didJwt from "did-jwt"; | ||||
|  | import { JWTDecoded } from "did-jwt/lib/JWT"; | ||||
|  | import { IIdentifier } from "@veramo/core"; | ||||
|  | import * as u8a from "uint8arrays"; | ||||
|  | 
 | ||||
|  | import { createDidPeerJwt } from "@/libs/crypto/vc/passkeyDidPeer"; | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  * Meta info about a key | ||||
|  |  */ | ||||
|  | export interface KeyMeta { | ||||
|  |   /** | ||||
|  |    * Decentralized ID for the key | ||||
|  |    */ | ||||
|  |   did: string; | ||||
|  |   /** | ||||
|  |    * Stringified IIDentifier object from Veramo | ||||
|  |    */ | ||||
|  |   identity?: string; | ||||
|  |   /** | ||||
|  |    * The Webauthn credential ID in hex, if this is from a passkey | ||||
|  |    */ | ||||
|  |   passkeyCredIdHex?: string; | ||||
|  | } | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  * Tell whether a key is from a passkey | ||||
|  |  * @param keyMeta contains info about the key, whose passkeyCredIdHex determines if the key is from a passkey | ||||
|  |  */ | ||||
|  | export function isFromPasskey(keyMeta?: KeyMeta): boolean { | ||||
|  |   return !!keyMeta?.passkeyCredIdHex; | ||||
|  | } | ||||
|  | 
 | ||||
|  | export async function createEndorserJwtForKey( | ||||
|  |   account: KeyMeta, | ||||
|  |   payload: object, | ||||
|  | ) { | ||||
|  |   if (account?.identity) { | ||||
|  |     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|  |     const identity: IIdentifier = JSON.parse(account.identity!); | ||||
|  |     const privateKeyHex = identity.keys[0].privateKeyHex; | ||||
|  |     const signer = await SimpleSigner(privateKeyHex as string); | ||||
|  |     return didJwt.createJWT(payload, { | ||||
|  |       issuer: account.did, | ||||
|  |       signer: signer, | ||||
|  |     }); | ||||
|  |   } else if (account?.passkeyCredIdHex) { | ||||
|  |     return createDidPeerJwt(account.did, account.passkeyCredIdHex, payload); | ||||
|  |   } else { | ||||
|  |     throw new Error("No identity data found to sign for DID " + account.did); | ||||
|  |   } | ||||
|  | } | ||||
|  | 
 | ||||
|  | /** | ||||
|  |  *  Copied out of did-jwt since it's deprecated in that library. | ||||
|  |  * | ||||
|  |  *  The SimpleSigner returns a configured function for signing data. | ||||
|  |  * | ||||
|  |  *  @example | ||||
|  |  *  const signer = SimpleSigner(import.meta.env.PRIVATE_KEY) | ||||
|  |  *  signer(data, (err, signature) => { | ||||
|  |  *    ... | ||||
|  |  *  }) | ||||
|  |  * | ||||
|  |  *  @param    {String}         hexPrivateKey    a hex encoded private key | ||||
|  |  *  @return   {Function}                     a configured signer function | ||||
|  |  */ | ||||
|  | function SimpleSigner(hexPrivateKey: string): didJwt.Signer { | ||||
|  |   const signer = didJwt.ES256KSigner(didJwt.hexToBytes(hexPrivateKey), true); | ||||
|  |   return async (data) => { | ||||
|  |     const signature = (await signer(data)) as string; | ||||
|  |     return fromJose(signature); | ||||
|  |   }; | ||||
|  | } | ||||
|  | 
 | ||||
|  | // from did-jwt/util; see SimpleSigner above
 | ||||
|  | function fromJose(signature: string): { | ||||
|  |   r: string; | ||||
|  |   s: string; | ||||
|  |   recoveryParam?: number; | ||||
|  | } { | ||||
|  |   const signatureBytes: Uint8Array = didJwt.base64ToBytes(signature); | ||||
|  |   if (signatureBytes.length < 64 || signatureBytes.length > 65) { | ||||
|  |     throw new TypeError( | ||||
|  |       `Wrong size for signature. Expected 64 or 65 bytes, but got ${signatureBytes.length}`, | ||||
|  |     ); | ||||
|  |   } | ||||
|  |   const r = bytesToHex(signatureBytes.slice(0, 32)); | ||||
|  |   const s = bytesToHex(signatureBytes.slice(32, 64)); | ||||
|  |   const recoveryParam = | ||||
|  |     signatureBytes.length === 65 ? signatureBytes[64] : undefined; | ||||
|  |   return { r, s, recoveryParam }; | ||||
|  | } | ||||
|  | 
 | ||||
|  | // from did-jwt/util; see SimpleSigner above
 | ||||
|  | function bytesToHex(b: Uint8Array): string { | ||||
|  |   return u8a.toString(b, "base16"); | ||||
|  | } | ||||
|  | 
 | ||||
|  | export function decodeEndorserJwt(jwt: string): JWTDecoded { | ||||
|  |   return didJwt.decodeJWT(jwt); | ||||
|  | } | ||||
					Loading…
					
					
				
		Reference in new issue