|
|
@ -1,11 +1,10 @@ |
|
|
|
import asn1 from "asn1-ber"; |
|
|
|
import { Buffer } from "buffer/"; |
|
|
|
import { decode as cborDecode } from "cbor-x"; |
|
|
|
import { createJWS, JWTPayload, verifyJWT } from "did-jwt"; |
|
|
|
import { getResolver } from "@veramo/did-provider-peer"; |
|
|
|
import { DIDResolutionResult, Resolver } from "did-resolver"; |
|
|
|
import { bytesToMultibase } from "@veramo/utils"; |
|
|
|
|
|
|
|
import { generateRandomBytes } from "@/libs/crypto"; |
|
|
|
|
|
|
|
export interface JWK { |
|
|
|
kty: string; |
|
|
|
crv: string; |
|
|
@ -102,7 +101,7 @@ export function createPeerDid(publicKeyCose: Uint8Array) { |
|
|
|
const methodSpecificId = bytesToMultibase( |
|
|
|
publicKeyCose, |
|
|
|
"base58btc", |
|
|
|
"ed25519-pub", |
|
|
|
"secp256k1-pub", |
|
|
|
); |
|
|
|
return "did:peer:0" + methodSpecificId; |
|
|
|
} |
|
|
@ -128,18 +127,53 @@ export async function createJwt( |
|
|
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
|
|
|
return async (data: string | Uint8Array) => { |
|
|
|
// also has clientDataJSON
|
|
|
|
const { signature, authenticatorData } = await generateWebAuthnSignature( |
|
|
|
data, |
|
|
|
credentialID, |
|
|
|
); |
|
|
|
const { signature } = await generateWebAuthnSignature(data, credentialID); |
|
|
|
|
|
|
|
// 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("signature inside signer", signature); |
|
|
|
console.log("buffer signature inside signer", signatureBuffer); |
|
|
|
// Decode the DER-encoded signature to extract R and S values
|
|
|
|
const reader = new asn1.BerReader(signatureBuffer); |
|
|
|
console.log("after reader"); |
|
|
|
reader.readSequence(); |
|
|
|
console.log("after read sequence"); |
|
|
|
const r = reader.readString(asn1.Ber.Integer, true); |
|
|
|
console.log("after r"); |
|
|
|
const s = reader.readString(asn1.Ber.Integer, true); |
|
|
|
console.log("after r & s"); |
|
|
|
|
|
|
|
// Combine the WebAuthn components into a single signature format as required by did-jwt
|
|
|
|
const combinedSignature = Buffer.concat([ |
|
|
|
Buffer.from(authenticatorData), |
|
|
|
Buffer.from(signature), |
|
|
|
]); |
|
|
|
// Ensure R and S are 32 bytes each
|
|
|
|
const rBuffer = Buffer.from(r); |
|
|
|
const sBuffer = Buffer.from(s); |
|
|
|
console.log("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; |
|
|
|
|
|
|
|
return combinedSignature.toString("base64"); |
|
|
|
// Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
|
|
|
|
const combinedSignature = Buffer.concat([rPadded, sPadded]); |
|
|
|
console.log( |
|
|
|
"combinedSignature", |
|
|
|
combinedSignature.length, |
|
|
|
combinedSignature, |
|
|
|
); |
|
|
|
|
|
|
|
const combSig64 = combinedSignature.toString("base64"); |
|
|
|
console.log("combSig64", combSig64); |
|
|
|
const combSig64Url = combSig64 |
|
|
|
.replace(/\+/g, "-") |
|
|
|
.replace(/\//g, "_") |
|
|
|
.replace(/=+$/, ""); |
|
|
|
console.log("combSig64Url", combSig64Url); |
|
|
|
return combSig64Url; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
@ -150,17 +184,25 @@ async function generateWebAuthnSignature( |
|
|
|
if (!(dataToSign instanceof Uint8Array)) { |
|
|
|
dataToSign = new TextEncoder().encode(dataToSign as string); |
|
|
|
} |
|
|
|
const challenge = generateRandomBytes(32); |
|
|
|
|
|
|
|
const options = { |
|
|
|
challenge: challenge, |
|
|
|
challenge: dataToSign, |
|
|
|
allowCredentials: [{ id: credentialId, type: "public-key" }], |
|
|
|
userVerification: "preferred", |
|
|
|
}; |
|
|
|
|
|
|
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
|
|
|
console.log("assertion", assertion); |
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
|
console.log( |
|
|
|
"clientDataJSON decoded", |
|
|
|
JSON.parse( |
|
|
|
new TextDecoder("utf-8").decode( |
|
|
|
authenticatorAssertionResponse.clientDataJSON, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
return { |
|
|
|
signature: authenticatorAssertionResponse.signature, |
|
|
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON, |
|
|
@ -170,15 +212,55 @@ async function generateWebAuthnSignature( |
|
|
|
|
|
|
|
export async function verifyJwt( |
|
|
|
jwt: string, |
|
|
|
issuerDid: string, |
|
|
|
publicKey: JWK, |
|
|
|
issuerDid: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
|
|
publicKey: JWK, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
|
|
) { |
|
|
|
const decoded = verifyJWT(jwt, { |
|
|
|
didAuthenticator: { |
|
|
|
authenticators: [{ publicKeyJwk: publicKey }], |
|
|
|
issuer: issuerDid, |
|
|
|
}, |
|
|
|
resolver: getResolver(), |
|
|
|
// didAuthenticator: {
|
|
|
|
// authenticators: [{ publicKeyJwk: publicKey }],
|
|
|
|
// issuer: issuerDid,
|
|
|
|
// },
|
|
|
|
//resolver: new Resolver({ ...getResolver() }),
|
|
|
|
resolver: new Resolver({ peer: peerDidToDidDocument }), |
|
|
|
}); |
|
|
|
return decoded; |
|
|
|
} |
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
|
|
|
if (!did.startsWith("did:peer:0z")) { |
|
|
|
throw new Error( |
|
|
|
"This only verifies a peer DID method 0 base58btc encoded.", |
|
|
|
); |
|
|
|
} |
|
|
|
// this is basically hard-coded based on the results from 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:peer:" + id + "#" + encnumbasis], |
|
|
|
authentication: ["did:peer:" + id + "#" + encnumbasis], |
|
|
|
capabilityDelegation: ["did:peer:" + id + "#" + encnumbasis], |
|
|
|
capabilityInvocation: ["did:peer:" + id + "#" + encnumbasis], |
|
|
|
id: did, |
|
|
|
keyAgreement: undefined, |
|
|
|
service: undefined, |
|
|
|
verificationMethod: [ |
|
|
|
{ |
|
|
|
id: "did:peer:" + id + "#" + encnumbasis, |
|
|
|
type: "EcdsaSecp256k1VerificationKey2019", |
|
|
|
controller: did, |
|
|
|
publicKeyMultibase: multibase, |
|
|
|
}, |
|
|
|
], |
|
|
|
}; |
|
|
|
return { |
|
|
|
didDocument, |
|
|
|
didDocumentMetadata: {}, |
|
|
|
didResolutionMetadata: { contentType: "application/did+ld+json" }, |
|
|
|
}; |
|
|
|
} |
|
|
|