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>passkey-cache
trentlarson
4 months 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