|
|
@ -4,6 +4,10 @@ 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 { |
|
|
|
verifyRegistrationResponse, |
|
|
|
VerifyRegistrationResponseOpts, |
|
|
|
} from "@simplewebauthn/server"; |
|
|
|
|
|
|
|
export interface JWK { |
|
|
|
kty: string; |
|
|
@ -16,6 +20,14 @@ export interface PublicKeyCredential { |
|
|
|
jwt: JWK; |
|
|
|
} |
|
|
|
|
|
|
|
function toBase64Url(anything: Uint8Array) { |
|
|
|
return Buffer.from(anything) |
|
|
|
.toString("base64") |
|
|
|
.replace(/\+/g, "-") |
|
|
|
.replace(/\//g, "_") |
|
|
|
.replace(/=+$/, ""); |
|
|
|
} |
|
|
|
|
|
|
|
export async function registerCredential( |
|
|
|
userId: Uint8Array, |
|
|
|
challenge: Uint8Array, |
|
|
@ -49,7 +61,30 @@ 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", |
|
|
|
); |
|
|
|
const attestationResponse = credential?.response; |
|
|
|
const verfInput: VerifyRegistrationResponseOpts = { |
|
|
|
response: { |
|
|
|
id: credential?.id as string, |
|
|
|
rawId: credential?.id as string, //Buffer.from(credential?.rawId).toString("base64"),
|
|
|
|
response: { |
|
|
|
attestationObject: toBase64Url(attestationResponse?.attestationObject), |
|
|
|
clientDataJSON: toBase64Url(attestationResponse?.clientDataJSON), |
|
|
|
}, |
|
|
|
clientExtensionResults: {}, |
|
|
|
type: "public-key", |
|
|
|
}, |
|
|
|
//expectedChallenge: Buffer.from(challenge).toString("base64"),
|
|
|
|
expectedChallenge: toBase64Url(challenge), |
|
|
|
expectedOrigin: window.location.origin, |
|
|
|
}; |
|
|
|
console.log("verfInput", verfInput); |
|
|
|
const verification = await verifyRegistrationResponse(verfInput); |
|
|
|
console.log("verification", verification); |
|
|
|
|
|
|
|
// Parse the attestation response to get the public key
|
|
|
|
const clientDataJSON = attestationResponse.clientDataJSON; |
|
|
@ -63,43 +98,50 @@ export async function registerCredential( |
|
|
|
); |
|
|
|
console.log("attestationObject", attestationObject); |
|
|
|
|
|
|
|
const publicKeyCose = extractPublicKeyCose(attestationObject.authData); |
|
|
|
const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData); |
|
|
|
const credData = parseAuthData(attestationObject.authData); |
|
|
|
console.log("new attempt at publicKey", credData); |
|
|
|
|
|
|
|
return { rawId: credential?.rawId, publicKeyJwk, publicKeyCose }; |
|
|
|
} |
|
|
|
const jwkObj = cborDecode( |
|
|
|
verification.registrationInfo?.credentialPublicKey as Uint8Array, |
|
|
|
); |
|
|
|
console.log("jwkObj from verification", jwkObj); |
|
|
|
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: toBase64Url(jwkObj[-2]), |
|
|
|
y: toBase64Url(jwkObj[-3]), |
|
|
|
}; |
|
|
|
const publicKeyBytes = Buffer.concat([ |
|
|
|
Buffer.from(jwkObj[-2]), |
|
|
|
Buffer.from(jwkObj[-3]), |
|
|
|
]); |
|
|
|
|
|
|
|
function extractPublicKeyJwk(authData: Uint8Array) { |
|
|
|
const publicKeyCose = extractPublicKeyCose(authData); // Example position
|
|
|
|
const publicKeyJwk = coseToJwk(publicKeyCose); |
|
|
|
return publicKeyJwk; |
|
|
|
} |
|
|
|
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
|
|
|
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
|
|
|
|
|
|
|
function extractPublicKeyCose(authData: Uint8Array) { |
|
|
|
// Extract the public key from authData using appropriate parsing.
|
|
|
|
// This involves extracting the COSE key format.
|
|
|
|
// For simplicity, we'll assume the public key is at a certain position in authData.
|
|
|
|
// Alternatively, see last answer here: https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
|
|
|
const publicKeyCose = authData.slice(authData.length - 77); |
|
|
|
return publicKeyCose; |
|
|
|
return { rawId: credential?.rawId, publicKeyJwk, publicKeyBytes }; |
|
|
|
} |
|
|
|
|
|
|
|
function coseToJwk(coseKey: Uint8Array) { |
|
|
|
// 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)), |
|
|
|
}; |
|
|
|
} |
|
|
|
// parse authData
|
|
|
|
// here's one: https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
|
|
|
|
// from https://chatgpt.com/c/0ce72fda-bc5d-42ff-a748-6022f6e39fa0
|
|
|
|
// from https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
|
|
|
|
|
|
|
export function createPeerDid(publicKeyCose: Uint8Array) { |
|
|
|
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( |
|
|
|
publicKeyCose, |
|
|
|
publicKeyBytes, |
|
|
|
"base58btc", |
|
|
|
"secp256k1-pub", |
|
|
|
); |
|
|
@ -216,21 +258,15 @@ export async function verifyJwt( |
|
|
|
publicKey: JWK, // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
|
|
) { |
|
|
|
const decoded = verifyJWT(jwt, { |
|
|
|
// 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 only verifies a peer DID method 0 encoded base58btc.", |
|
|
|
); |
|
|
|
} |
|
|
|
// this is basically hard-coded based on the results from the @aviarytech/did-peer resolver
|
|
|
@ -242,19 +278,19 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> { |
|
|
|
"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], |
|
|
|
assertionMethod: [did + "#" + encnumbasis], |
|
|
|
authentication: [did + "#" + encnumbasis], |
|
|
|
capabilityDelegation: [did + "#" + encnumbasis], |
|
|
|
capabilityInvocation: [did + "#" + encnumbasis], |
|
|
|
id: did, |
|
|
|
keyAgreement: undefined, |
|
|
|
service: undefined, |
|
|
|
verificationMethod: [ |
|
|
|
{ |
|
|
|
id: "did:peer:" + id + "#" + encnumbasis, |
|
|
|
type: "EcdsaSecp256k1VerificationKey2019", |
|
|
|
controller: did, |
|
|
|
id: did + "#" + encnumbasis, |
|
|
|
publicKeyMultibase: multibase, |
|
|
|
type: "EcdsaSecp256k1VerificationKey2019", |
|
|
|
}, |
|
|
|
], |
|
|
|
}; |
|
|
|