move low-level DID-related create & decode into separate folder #120
 Merged
	
	
		
		
			
		
		
		
			
		
		
			
			
				trentlarson
				merged 2 commits from lib-refactor into master 1 year ago
			
		
	
	
				 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