|
@ -1,17 +1,35 @@ |
|
|
|
|
|
import { Buffer } from "buffer/"; |
|
|
import { decode as cborDecode } from "cbor-x"; |
|
|
import { decode as cborDecode } from "cbor-x"; |
|
|
import { createJWS, JWTPayload } from "did-jwt"; |
|
|
import { createJWS, JWTPayload, verifyJWT } from "did-jwt"; |
|
|
|
|
|
import { getResolver } from "@veramo/did-provider-peer"; |
|
|
|
|
|
|
|
|
export async function registerCredential() { |
|
|
import { generateRandomBytes } from "@/libs/crypto"; |
|
|
|
|
|
|
|
|
|
|
|
export interface JWK { |
|
|
|
|
|
kty: string; |
|
|
|
|
|
crv: string; |
|
|
|
|
|
x: string; |
|
|
|
|
|
y: string; |
|
|
|
|
|
} |
|
|
|
|
|
export interface PublicKeyCredential { |
|
|
|
|
|
rawId: Uint8Array; |
|
|
|
|
|
jwt: JWK; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function registerCredential( |
|
|
|
|
|
userId: Uint8Array, |
|
|
|
|
|
challenge: Uint8Array, |
|
|
|
|
|
) { |
|
|
const publicKeyOptions: PublicKeyCredentialCreationOptions = { |
|
|
const publicKeyOptions: PublicKeyCredentialCreationOptions = { |
|
|
challenge: new Uint8Array(32), // Random challenge
|
|
|
challenge: challenge, |
|
|
rp: { |
|
|
rp: { |
|
|
name: "Time Safari", |
|
|
name: "Time Safari", |
|
|
id: window.location.hostname, |
|
|
id: window.location.hostname, |
|
|
}, |
|
|
}, |
|
|
user: { |
|
|
user: { |
|
|
id: new Uint8Array(16), // User ID
|
|
|
id: userId, |
|
|
name: "user@example.com", |
|
|
name: "current-user", |
|
|
displayName: "Example User", |
|
|
displayName: "Current User", |
|
|
}, |
|
|
}, |
|
|
pubKeyCredParams: [ |
|
|
pubKeyCredParams: [ |
|
|
{ |
|
|
{ |
|
@ -35,19 +53,23 @@ export async function registerCredential() { |
|
|
|
|
|
|
|
|
// Parse the attestation response to get the public key
|
|
|
// Parse the attestation response to get the public key
|
|
|
const clientDataJSON = attestationResponse.clientDataJSON; |
|
|
const clientDataJSON = attestationResponse.clientDataJSON; |
|
|
console.log("clientDataJSON", clientDataJSON); |
|
|
console.log("clientDataJSON raw", clientDataJSON); |
|
|
|
|
|
console.log( |
|
|
|
|
|
"clientDataJSON dec", |
|
|
|
|
|
new TextDecoder("utf-8").decode(clientDataJSON), |
|
|
|
|
|
); |
|
|
const attestationObject = cborDecode( |
|
|
const attestationObject = cborDecode( |
|
|
new Uint8Array(attestationResponse.attestationObject), |
|
|
new Uint8Array(attestationResponse.attestationObject), |
|
|
); |
|
|
); |
|
|
|
|
|
console.log("attestationObject", attestationObject); |
|
|
|
|
|
|
|
|
const authData = new Uint8Array(attestationObject.authData); |
|
|
const authData = new Uint8Array(attestationObject.authData); |
|
|
const publicKey = extractPublicKey(authData); |
|
|
const publicKey = extractPublicKey(authData); |
|
|
|
|
|
|
|
|
return publicKey; |
|
|
return { rawId: credential?.rawId, publicKey }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// @ts-expect-error just because it doesn't like the "any"
|
|
|
function extractPublicKey(authData: Uint8Array) { |
|
|
function extractPublicKey(authData) { |
|
|
|
|
|
// Extract the public key from authData using appropriate parsing
|
|
|
// Extract the public key from authData using appropriate parsing
|
|
|
// This involves extracting the COSE key format and converting it to JWK
|
|
|
// This involves extracting the COSE key format and converting it to JWK
|
|
|
// For simplicity, we'll assume the public key is at a certain position in authData
|
|
|
// For simplicity, we'll assume the public key is at a certain position in authData
|
|
@ -56,8 +78,7 @@ function extractPublicKey(authData) { |
|
|
return publicKeyJwk; |
|
|
return publicKeyJwk; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// @ts-expect-error just because it doesn't like the "any"
|
|
|
function coseToJwk(coseKey: Uint8Array) { |
|
|
function coseToJwk(coseKey) { |
|
|
|
|
|
// Convert COSE key format to JWK
|
|
|
// Convert COSE key format to JWK
|
|
|
// This is simplified and needs appropriate parsing and conversion logic
|
|
|
// This is simplified and needs appropriate parsing and conversion logic
|
|
|
return { |
|
|
return { |
|
@ -68,29 +89,22 @@ function coseToJwk(coseKey) { |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function generateWebAuthnSignature( |
|
|
export async function createJwt( |
|
|
dataToSign: string | Uint8Array, |
|
|
payload: object, |
|
|
credentialID: ArrayBuffer, |
|
|
issuerDid: string, |
|
|
|
|
|
credentialId: ArrayBuffer, |
|
|
) { |
|
|
) { |
|
|
if (!(dataToSign instanceof Uint8Array)) { |
|
|
const signer = await webAuthnES256KSigner(credentialId); |
|
|
dataToSign = new TextEncoder().encode(dataToSign as string); |
|
|
|
|
|
} |
|
|
|
|
|
const challenge = dataToSign; |
|
|
|
|
|
|
|
|
|
|
|
const options = { |
|
|
// from createJWT in did-jwt/src/JWT.ts
|
|
|
challenge: challenge, |
|
|
const header: JWTPayload = { typ: "JWT", alg: "ES256K" }; |
|
|
allowCredentials: [{ id: credentialID, type: "public-key" }], |
|
|
const timestamps: Partial<JWTPayload> = { |
|
|
userVerification: "preferred", |
|
|
iat: Math.floor(Date.now() / 1000), |
|
|
|
|
|
exp: undefined, |
|
|
}; |
|
|
}; |
|
|
|
|
|
const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; |
|
|
|
|
|
|
|
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
|
|
return createJWS(fullPayload, signer, header); |
|
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
|
|
|
return { |
|
|
|
|
|
signature: authenticatorAssertionResponse.signature, |
|
|
|
|
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON, |
|
|
|
|
|
authenticatorData: authenticatorAssertionResponse.authenticatorData, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
|
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
|
@ -111,20 +125,37 @@ async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
|
|
}; |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export async function createJwt( |
|
|
async function generateWebAuthnSignature( |
|
|
payload: object, |
|
|
dataToSign: string | Uint8Array, |
|
|
issuerDid: string, |
|
|
|
|
|
credentialId: ArrayBuffer, |
|
|
credentialId: ArrayBuffer, |
|
|
) { |
|
|
) { |
|
|
const signer = await webAuthnES256KSigner(credentialId); |
|
|
if (!(dataToSign instanceof Uint8Array)) { |
|
|
|
|
|
dataToSign = new TextEncoder().encode(dataToSign as string); |
|
|
|
|
|
} |
|
|
|
|
|
const challenge = generateRandomBytes(32); |
|
|
|
|
|
|
|
|
// from createJWT in did-jwt/src/JWT.ts
|
|
|
const options = { |
|
|
const header: JWTPayload = { typ: "JWT", alg: "ES256K" }; |
|
|
challenge: challenge, |
|
|
const timestamps: Partial<JWTPayload> = { |
|
|
allowCredentials: [{ id: credentialId, type: "public-key" }], |
|
|
iat: Math.floor(Date.now() / 1000), |
|
|
userVerification: "preferred", |
|
|
exp: undefined, |
|
|
|
|
|
}; |
|
|
}; |
|
|
const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; |
|
|
|
|
|
|
|
|
|
|
|
return createJWS(fullPayload, signer, header); |
|
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
|
|
|
|
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
|
|
|
return { |
|
|
|
|
|
signature: authenticatorAssertionResponse.signature, |
|
|
|
|
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON, |
|
|
|
|
|
authenticatorData: authenticatorAssertionResponse.authenticatorData, |
|
|
|
|
|
}; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export async function verifyJwt(jwt: string, publicKey: JWK) { |
|
|
|
|
|
const decoded = verifyJWT(jwt, { |
|
|
|
|
|
didAuthenticator: { |
|
|
|
|
|
authenticators: [{ publicKeyJwk: publicKey }], |
|
|
|
|
|
}, |
|
|
|
|
|
resolver: getResolver(), |
|
|
|
|
|
}); |
|
|
|
|
|
return decoded; |
|
|
} |
|
|
} |
|
|