|
|
@ -4,10 +4,18 @@ import { decode as cborDecode } from "cbor-x"; |
|
|
|
import { createJWS, JWTPayload, verifyJWT } from "did-jwt"; |
|
|
|
import { DIDResolutionResult, Resolver } from "did-resolver"; |
|
|
|
import { bytesToMultibase } from "@veramo/utils"; |
|
|
|
import { startAuthentication } from "@simplewebauthn/browser"; |
|
|
|
import { |
|
|
|
generateAuthenticationOptions, |
|
|
|
verifyAuthenticationResponse, |
|
|
|
verifyRegistrationResponse, |
|
|
|
VerifyRegistrationResponseOpts, |
|
|
|
} from "@simplewebauthn/server"; |
|
|
|
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; |
|
|
|
import { |
|
|
|
Base64URLString, |
|
|
|
PublicKeyCredentialRequestOptionsJSON, |
|
|
|
} from "@simplewebauthn/types"; |
|
|
|
|
|
|
|
export interface JWK { |
|
|
|
kty: string; |
|
|
@ -61,11 +69,8 @@ export async function registerCredential( |
|
|
|
publicKey: publicKeyOptions, |
|
|
|
}); |
|
|
|
console.log("credential", credential); |
|
|
|
console.log(credential?.id, " is the new Id"); |
|
|
|
console.log( |
|
|
|
Buffer.from(credential?.rawId).toString("base64"), |
|
|
|
" is the base64 rawId", |
|
|
|
); |
|
|
|
console.log(credential?.id, " is the new ID base64-url-encoded"); |
|
|
|
console.log(toBase64Url(credential?.rawId), " is the base64 rawId"); |
|
|
|
const attestationResponse = credential?.response; |
|
|
|
const verfInput: VerifyRegistrationResponseOpts = { |
|
|
|
response: { |
|
|
@ -78,7 +83,6 @@ export async function registerCredential( |
|
|
|
clientExtensionResults: {}, |
|
|
|
type: "public-key", |
|
|
|
}, |
|
|
|
//expectedChallenge: Buffer.from(challenge).toString("base64"),
|
|
|
|
expectedChallenge: toBase64Url(challenge), |
|
|
|
expectedOrigin: window.location.origin, |
|
|
|
}; |
|
|
@ -98,9 +102,6 @@ export async function registerCredential( |
|
|
|
); |
|
|
|
console.log("attestationObject", attestationObject); |
|
|
|
|
|
|
|
const credData = parseAuthData(attestationObject.authData); |
|
|
|
console.log("new attempt at publicKey", credData); |
|
|
|
|
|
|
|
const jwkObj = cborDecode( |
|
|
|
verification.registrationInfo?.credentialPublicKey as Uint8Array, |
|
|
|
); |
|
|
@ -129,7 +130,13 @@ export async function registerCredential( |
|
|
|
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
|
|
|
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
|
|
|
|
|
|
|
return { rawId: credential?.rawId, publicKeyJwk, publicKeyBytes }; |
|
|
|
return { |
|
|
|
authData: attestationObject.authData, |
|
|
|
credId: credential?.id, |
|
|
|
rawId: credential?.rawId, |
|
|
|
publicKeyJwk, |
|
|
|
publicKeyBytes, |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
// parse authData
|
|
|
@ -148,115 +155,181 @@ export function createPeerDid(publicKeyBytes: Uint8Array) { |
|
|
|
return "did:peer:0" + methodSpecificId; |
|
|
|
} |
|
|
|
|
|
|
|
export async function createJwt( |
|
|
|
payload: object, |
|
|
|
issuerDid: string, |
|
|
|
credentialId: ArrayBuffer, |
|
|
|
) { |
|
|
|
const signer = await webAuthnES256KSigner(credentialId); |
|
|
|
export class PeerSetup { |
|
|
|
public authenticatorData?: ArrayBuffer; |
|
|
|
public clientDataJsonDecoded?: object; |
|
|
|
|
|
|
|
// 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 }; |
|
|
|
public async createJwt( |
|
|
|
payload: object, |
|
|
|
issuerDid: string, |
|
|
|
credentialId: string, |
|
|
|
) { |
|
|
|
const signer = await this.webAuthnES256KSigner(credentialId); |
|
|
|
|
|
|
|
return createJWS(fullPayload, signer, header); |
|
|
|
} |
|
|
|
// 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 }; |
|
|
|
|
|
|
|
async function webAuthnES256KSigner(credentialID: ArrayBuffer) { |
|
|
|
return async (data: string | Uint8Array) => { |
|
|
|
// also has clientDataJSON
|
|
|
|
const { signature } = await generateWebAuthnSignature(data, credentialID); |
|
|
|
const jwt = createJWS(fullPayload, signer, header); |
|
|
|
return jwt; |
|
|
|
} |
|
|
|
|
|
|
|
// 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"); |
|
|
|
async webAuthnES256KSigner(credentialID: string) { |
|
|
|
return async (data: string | Uint8Array) => { |
|
|
|
const signature = await this.generateWebAuthnSignature( |
|
|
|
data, |
|
|
|
credentialID, |
|
|
|
); |
|
|
|
|
|
|
|
// 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; |
|
|
|
// 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"); |
|
|
|
|
|
|
|
// 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, |
|
|
|
); |
|
|
|
// 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; |
|
|
|
|
|
|
|
const combSig64 = combinedSignature.toString("base64"); |
|
|
|
console.log("combSig64", combSig64); |
|
|
|
const combSig64Url = combSig64 |
|
|
|
.replace(/\+/g, "-") |
|
|
|
.replace(/\//g, "_") |
|
|
|
.replace(/=+$/, ""); |
|
|
|
console.log("combSig64Url", combSig64Url); |
|
|
|
return combSig64Url; |
|
|
|
}; |
|
|
|
} |
|
|
|
// 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, |
|
|
|
); |
|
|
|
|
|
|
|
async function generateWebAuthnSignature( |
|
|
|
dataToSign: string | Uint8Array, |
|
|
|
credentialId: ArrayBuffer, |
|
|
|
) { |
|
|
|
if (!(dataToSign instanceof Uint8Array)) { |
|
|
|
dataToSign = new TextEncoder().encode(dataToSign as string); |
|
|
|
const combSig64 = combinedSignature.toString("base64"); |
|
|
|
console.log("combSig64", combSig64); |
|
|
|
const combSig64Url = combSig64 |
|
|
|
.replace(/\+/g, "-") |
|
|
|
.replace(/\//g, "_") |
|
|
|
.replace(/=+$/, ""); |
|
|
|
console.log("combSig64Url", combSig64Url); |
|
|
|
return combSig64Url; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
const options = { |
|
|
|
challenge: dataToSign, |
|
|
|
allowCredentials: [{ id: credentialId, type: "public-key" }], |
|
|
|
userVerification: "preferred", |
|
|
|
}; |
|
|
|
async generateWebAuthnSignature( |
|
|
|
dataToSign: string | Uint8Array, // from Signer interface
|
|
|
|
credentialId: string, |
|
|
|
) { |
|
|
|
if (dataToSign instanceof Uint8Array) { |
|
|
|
dataToSign = new TextDecoder("utf-8").decode(dataToSign as Uint8Array); |
|
|
|
} |
|
|
|
|
|
|
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
|
|
|
console.log("assertion", assertion); |
|
|
|
console.log("credentialId", credentialId); |
|
|
|
const options = { |
|
|
|
challenge: new TextEncoder().encode(dataToSign).buffer, |
|
|
|
//allowCredentials: [{ id: credentialId, type: "public-key" }],
|
|
|
|
userVerification: "preferred", |
|
|
|
}; |
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
|
console.log( |
|
|
|
"clientDataJSON decoded", |
|
|
|
JSON.parse( |
|
|
|
const assertion = await navigator.credentials.get({ publicKey: options }); |
|
|
|
console.log("assertion", assertion); |
|
|
|
|
|
|
|
const authenticatorAssertionResponse = assertion?.response; |
|
|
|
this.clientDataJsonDecoded = JSON.parse( |
|
|
|
new TextDecoder("utf-8").decode( |
|
|
|
authenticatorAssertionResponse.clientDataJSON, |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
return { |
|
|
|
signature: authenticatorAssertionResponse.signature, |
|
|
|
clientDataJSON: authenticatorAssertionResponse.clientDataJSON, |
|
|
|
authenticatorData: authenticatorAssertionResponse.authenticatorData, |
|
|
|
}; |
|
|
|
); |
|
|
|
console.log("clientDataJSON decoded", this.clientDataJsonDecoded); |
|
|
|
this.authenticatorData = authenticatorAssertionResponse.authenticatorData; |
|
|
|
console.log("authenticator data", this.authenticatorData); |
|
|
|
return authenticatorAssertionResponse.signature; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
export async function verifyJwt( |
|
|
|
jwt: string, |
|
|
|
issuerDid: string, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
|
|
publicKey: JWK, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
|
|
credId: Base64URLString, |
|
|
|
rawId: Uint8Array, |
|
|
|
authenticatorData: ArrayBuffer, |
|
|
|
clientDataJSON: object, |
|
|
|
publicKey: Uint8Array, |
|
|
|
signature: Base64URLString, |
|
|
|
) { |
|
|
|
const options: PublicKeyCredentialRequestOptionsJSON = |
|
|
|
await generateAuthenticationOptions({ |
|
|
|
rpID: window.location.hostname, |
|
|
|
// Require users to use a previously-registered authenticator
|
|
|
|
// allowCredentials: userPasskeys.map(passkey => ({
|
|
|
|
// id: passkey.id,
|
|
|
|
// transports: passkey.transports,
|
|
|
|
// })),
|
|
|
|
}); |
|
|
|
console.log("authentication options", options); |
|
|
|
|
|
|
|
const clientAuth = await startAuthentication(options); |
|
|
|
console.log("clientAuth", clientAuth); |
|
|
|
|
|
|
|
const verfOpts: VerifyAuthenticationResponseOpts = { |
|
|
|
response: clientAuth, |
|
|
|
authenticator: { |
|
|
|
credentialID: credId, |
|
|
|
credentialPublicKey: publicKey, |
|
|
|
counter: 0, |
|
|
|
}, |
|
|
|
expectedChallenge: () => true, // options.challenge doesn't work
|
|
|
|
expectedOrigin: window.location.origin, |
|
|
|
expectedRPID: window.location.hostname, |
|
|
|
}; |
|
|
|
console.log("verfOpts", verfOpts); |
|
|
|
const verificationFromClient = await verifyAuthenticationResponse(verfOpts); |
|
|
|
console.log("client auth verification", verificationFromClient); |
|
|
|
|
|
|
|
const authData = toBase64Url(Buffer.from(authenticatorData)); |
|
|
|
const bufferizedJson = toBase64Url( |
|
|
|
new TextEncoder().encode(JSON.stringify(clientDataJSON)), |
|
|
|
); |
|
|
|
const authOpts: VerifyAuthenticationResponseOpts = { |
|
|
|
response: { |
|
|
|
id: credId, |
|
|
|
rawId: toBase64Url(rawId), |
|
|
|
response: { |
|
|
|
authenticatorData: authData, |
|
|
|
clientDataJSON: bufferizedJson, |
|
|
|
signature: signature, |
|
|
|
}, |
|
|
|
clientExtensionResults: {}, |
|
|
|
type: "public-key", |
|
|
|
}, |
|
|
|
expectedChallenge: () => true, // options.challenge doesn't work
|
|
|
|
expectedOrigin: window.location.origin, |
|
|
|
expectedRPID: window.location.hostname, |
|
|
|
authenticator: { |
|
|
|
credentialID: credId, |
|
|
|
credentialPublicKey: publicKey, |
|
|
|
counter: 0, |
|
|
|
}, |
|
|
|
}; |
|
|
|
const verification = await verifyAuthenticationResponse(authOpts); |
|
|
|
console.log("auth verification", verification); |
|
|
|
|
|
|
|
const decoded = verifyJWT(jwt, { |
|
|
|
resolver: new Resolver({ peer: peerDidToDidDocument }), |
|
|
|
}); |
|
|
@ -266,7 +339,7 @@ export async function verifyJwt( |
|
|
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
|
|
|
if (!did.startsWith("did:peer:0z")) { |
|
|
|
throw new Error( |
|
|
|
"This only verifies a peer DID method 0 encoded base58btc.", |
|
|
|
"This only verifies a peer DID, method 0, encoded base58btc.", |
|
|
|
); |
|
|
|
} |
|
|
|
// this is basically hard-coded based on the results from the @aviarytech/did-peer resolver
|
|
|
|