Trent Larson
6 months ago
4 changed files with 266 additions and 2 deletions
@ -0,0 +1,130 @@ |
|||||
|
import { decode as cborDecode } from "cbor-x"; |
||||
|
import { createJWS, JWTPayload } from "did-jwt"; |
||||
|
|
||||
|
export async function registerCredential() { |
||||
|
const publicKeyOptions: PublicKeyCredentialCreationOptions = { |
||||
|
challenge: new Uint8Array(32), // Random challenge
|
||||
|
rp: { |
||||
|
name: "Time Safari", |
||||
|
id: window.location.hostname, |
||||
|
}, |
||||
|
user: { |
||||
|
id: new Uint8Array(16), // User ID
|
||||
|
name: "user@example.com", |
||||
|
displayName: "Example User", |
||||
|
}, |
||||
|
pubKeyCredParams: [ |
||||
|
{ |
||||
|
type: "public-key", |
||||
|
alg: -7, // ES256 algorithm
|
||||
|
}, |
||||
|
], |
||||
|
authenticatorSelection: { |
||||
|
authenticatorAttachment: "platform", |
||||
|
userVerification: "preferred", |
||||
|
}, |
||||
|
timeout: 60000, |
||||
|
attestation: "direct", |
||||
|
}; |
||||
|
|
||||
|
const credential = await navigator.credentials.create({ |
||||
|
publicKey: publicKeyOptions, |
||||
|
}); |
||||
|
console.log("credential", credential); |
||||
|
const attestationResponse = credential?.response; |
||||
|
|
||||
|
// Parse the attestation response to get the public key
|
||||
|
const clientDataJSON = attestationResponse.clientDataJSON; |
||||
|
console.log("clientDataJSON", clientDataJSON); |
||||
|
const attestationObject = cborDecode( |
||||
|
new Uint8Array(attestationResponse.attestationObject), |
||||
|
); |
||||
|
|
||||
|
const authData = new Uint8Array(attestationObject.authData); |
||||
|
const publicKey = extractPublicKey(authData); |
||||
|
|
||||
|
return publicKey; |
||||
|
} |
||||
|
|
||||
|
// @ts-expect-error just because it doesn't like the "any"
|
||||
|
function extractPublicKey(authData) { |
||||
|
// Extract the public key from authData using appropriate parsing
|
||||
|
// 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
|
||||
|
const publicKeyCose = authData.slice(authData.length - 77); // Example position
|
||||
|
const publicKeyJwk = coseToJwk(publicKeyCose); |
||||
|
return publicKeyJwk; |
||||
|
} |
||||
|
|
||||
|
// @ts-expect-error just because it doesn't like the "any"
|
||||
|
function coseToJwk(coseKey) { |
||||
|
// Convert COSE key format to JWK
|
||||
|
// This is simplified and needs appropriate parsing and conversion logic
|
||||
|
return { |
||||
|
kty: "EC", |
||||
|
crv: "P-256", |
||||
|
x: btoa(coseKey.slice(2, 34)), |
||||
|
y: btoa(coseKey.slice(34, 66)), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async function generateWebAuthnSignature( |
||||
|
dataToSign: string | Uint8Array, |
||||
|
credentialID: ArrayBuffer, |
||||
|
) { |
||||
|
if (!(dataToSign instanceof Uint8Array)) { |
||||
|
dataToSign = new TextEncoder().encode(dataToSign as string); |
||||
|
} |
||||
|
const challenge = dataToSign; |
||||
|
|
||||
|
const options = { |
||||
|
challenge: challenge, |
||||
|
allowCredentials: [{ id: credentialID, type: "public-key" }], |
||||
|
userVerification: "preferred", |
||||
|
}; |
||||
|
|
||||
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
||||
|
|
||||
|
const authenticatorAssertionResponse = assertion?.response; |
||||
|
return { |
||||
|
signature: authenticatorAssertionResponse.signature, |
||||
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON, |
||||
|
authenticatorData: authenticatorAssertionResponse.authenticatorData, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
||||
|
return async (data: string | Uint8Array) => { |
||||
|
// also has clientDataJSON
|
||||
|
const { signature, authenticatorData } = await generateWebAuthnSignature( |
||||
|
data, |
||||
|
credentialID, |
||||
|
); |
||||
|
|
||||
|
// Combine the WebAuthn components into a single signature format as required by did-jwt
|
||||
|
const combinedSignature = Buffer.concat([ |
||||
|
Buffer.from(authenticatorData), |
||||
|
Buffer.from(signature), |
||||
|
]); |
||||
|
|
||||
|
return combinedSignature.toString("base64"); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
export async function createJwt( |
||||
|
payload: object, |
||||
|
issuerDid: string, |
||||
|
credentialId: ArrayBuffer, |
||||
|
) { |
||||
|
const signer = await webAuthnES256KSigner(credentialId); |
||||
|
|
||||
|
// from createJWT in did-jwt/src/JWT.ts
|
||||
|
const header: JWTPayload = { typ: "JWT", alg: "ES256K" }; |
||||
|
const timestamps: Partial<JWTPayload> = { |
||||
|
iat: Math.floor(Date.now() / 1000), |
||||
|
exp: undefined, |
||||
|
}; |
||||
|
const fullPayload = { ...timestamps, ...payload, iss: issuerDid }; |
||||
|
|
||||
|
return createJWS(fullPayload, signer, header); |
||||
|
} |
Loading…
Reference in new issue