Browse Source

retrieve the correct passkey just created (doesn't validate JWT)

pull/116/head
Trent Larson 4 months ago
parent
commit
66aeb7ed29
  1. 134
      package-lock.json
  2. 1
      package.json
  3. 4
      src/libs/crypto/index.ts
  4. 115
      src/libs/didPeer.ts
  5. 2
      src/main.ts
  6. 37
      src/views/StartView.vue

134
package-lock.json

@ -23,6 +23,7 @@
"@veramo/data-store": "^5.6.0", "@veramo/data-store": "^5.6.0",
"@veramo/did-manager": "^5.6.0", "@veramo/did-manager": "^5.6.0",
"@veramo/did-provider-ethr": "^5.6.0", "@veramo/did-provider-ethr": "^5.6.0",
"@veramo/did-provider-peer": "^6.0.0",
"@veramo/did-resolver": "^5.6.0", "@veramo/did-resolver": "^5.6.0",
"@veramo/key-manager": "^5.6.0", "@veramo/key-manager": "^5.6.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",
@ -123,6 +124,14 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@aviarytech/did-peer": {
"version": "0.0.22",
"resolved": "https://registry.npmjs.org/@aviarytech/did-peer/-/did-peer-0.0.22.tgz",
"integrity": "sha512-BdA7L9wpYNLf1c3d0yB92aoj1AUWE10p408VZ4IJXfavb/oNxALZRRRJTcvMdrd5P2XXQsP5+x4bXfO24iRURg==",
"dependencies": {
"buffer": "^6.0.3"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.24.6", "version": "7.24.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz",
@ -9441,6 +9450,131 @@
"ethr-did": "^3.0.5" "ethr-did": "^3.0.5"
} }
}, },
"node_modules/@veramo/did-provider-peer": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@veramo/did-provider-peer/-/did-provider-peer-6.0.0.tgz",
"integrity": "sha512-U58pm/KaVtnQ15xLdJxtGDG2QhsPZgFxR0qE6X1S+p/zbI33K12nnUSvts6xrPUzoLXAETjPa1V1MA8SnXrJIg==",
"dependencies": {
"@aviarytech/did-peer": "^0.0.22",
"@veramo/core-types": "^6.0.0",
"@veramo/did-manager": "^6.0.0",
"@veramo/utils": "^6.0.0",
"debug": "^4.3.3",
"did-resolver": "^4.1.0"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/@noble/ciphers": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.5.3.tgz",
"integrity": "sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==",
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/@veramo/core-types": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@veramo/core-types/-/core-types-6.0.0.tgz",
"integrity": "sha512-Gdjo45veUT2DXhPmhD2hy1X9jem7IuZNjUvPIC5+qRVBT1yvWsJjOL7D+YWhbeA6LHIbNpO1a9GR2iaSL/FebA==",
"dependencies": {
"credential-status": "^3.0.0",
"debug": "^4.3.3",
"did-jwt-vc": "^4.0.0",
"did-resolver": "^4.1.0"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/@veramo/did-discovery": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@veramo/did-discovery/-/did-discovery-6.0.0.tgz",
"integrity": "sha512-S/r0MWAVQZSe9SGb7XfWDhdsLfsSqkzMIweOiLTjZjHndkkDJ+qX3s5gblSNcTTvOvZRsXDI3fa4IbsjhDc1PQ==",
"dependencies": {
"@veramo/core-types": "^6.0.0",
"debug": "^4.3.3"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/@veramo/did-manager": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@veramo/did-manager/-/did-manager-6.0.0.tgz",
"integrity": "sha512-sjF0d0ZE+h7Jw7WwGtyRhjT5MhKBwGLx2AsR2Ao0qqyloVAxVS9jDcl2DP1ZdCKC+7BR4X30E1ja99N7q3INDg==",
"dependencies": {
"@veramo/core-types": "^6.0.0",
"@veramo/did-discovery": "^6.0.0"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/@veramo/utils": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@veramo/utils/-/utils-6.0.0.tgz",
"integrity": "sha512-IvX0QqjLfamfD6s/qUVtfWFtdb1Idd5Dvfm7RzMKSvT+D5cstFQw3v/sySKoEY8du/ruy8ZUe7v9VpCAYlngPQ==",
"dependencies": {
"@ipld/dag-pb": "^4.0.5",
"@noble/curves": "^1.1.0",
"@veramo/core-types": "^6.0.0",
"credential-status": "^3.0.0",
"cross-fetch": "^4.0.0",
"debug": "^4.3.3",
"did-jwt": "^8.0.0",
"did-jwt-vc": "^4.0.0",
"did-resolver": "^4.1.0",
"ethers": "^6.11.1",
"ipfs-unixfs": "^11.1.0",
"multiformats": "^13.0.0",
"uint8arrays": "^4.0.6"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/credential-status": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/credential-status/-/credential-status-3.0.4.tgz",
"integrity": "sha512-6xHMXhdIZjhyTzkXx49hR4CjHNEqFJmWEL29CNulGA0XDozHsZXVF34BZyHGz9vWD/Du05pVEDhpZL0jdLGIRw==",
"dependencies": {
"did-jwt": "^8.0.0",
"did-resolver": "^4.1.0"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt": {
"version": "8.0.4",
"resolved": "https://registry.npmjs.org/did-jwt/-/did-jwt-8.0.4.tgz",
"integrity": "sha512-KPtG7H+8GgKGMiDqFvOdNy5BBN3hpA+8xV7VygEnpst5oPIqjvcH3rTtnPF55a8bOxIzE2PudKGIXIQhekv7WA==",
"dependencies": {
"@noble/ciphers": "^0.5.0",
"@noble/curves": "^1.0.0",
"@noble/hashes": "^1.3.0",
"@scure/base": "^1.1.3",
"canonicalize": "^2.0.0",
"did-resolver": "^4.1.0",
"multibase": "^4.0.6",
"multiformats": "^9.6.2",
"uint8arrays": "3.1.1"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt-vc": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/did-jwt-vc/-/did-jwt-vc-4.0.4.tgz",
"integrity": "sha512-O/VSW+pux25+bERGQ1Z+LNv8z8gIy++yL8yjspxNZHCLjdmCelZz3hjuIpHlixhBCFP2YyTOQqGg8QeIMRwN9Q==",
"dependencies": {
"did-jwt": "^8.0.0",
"did-resolver": "^4.1.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt/node_modules/multiformats": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg=="
},
"node_modules/@veramo/did-provider-peer/node_modules/did-jwt/node_modules/uint8arrays": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.1.tgz",
"integrity": "sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg==",
"dependencies": {
"multiformats": "^9.4.2"
}
},
"node_modules/@veramo/did-provider-peer/node_modules/multiformats": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-13.1.0.tgz",
"integrity": "sha512-HzdtdBwxsIkzpeXzhQ5mAhhuxcHbjEHH+JQoxt7hG/2HGFjjwyolLo7hbaexcnhoEuV4e0TNJ8kkpMjiEYY4VQ=="
},
"node_modules/@veramo/did-resolver": { "node_modules/@veramo/did-resolver": {
"version": "5.6.0", "version": "5.6.0",
"license": "Apache-2.0", "license": "Apache-2.0",

1
package.json

@ -26,6 +26,7 @@
"@veramo/data-store": "^5.6.0", "@veramo/data-store": "^5.6.0",
"@veramo/did-manager": "^5.6.0", "@veramo/did-manager": "^5.6.0",
"@veramo/did-provider-ethr": "^5.6.0", "@veramo/did-provider-ethr": "^5.6.0",
"@veramo/did-provider-peer": "^6.0.0",
"@veramo/did-resolver": "^5.6.0", "@veramo/did-resolver": "^5.6.0",
"@veramo/key-manager": "^5.6.0", "@veramo/key-manager": "^5.6.0",
"@vueuse/core": "^10.9.0", "@vueuse/core": "^10.9.0",

4
src/libs/crypto/index.ts

@ -64,6 +64,10 @@ export const deriveAddress = (
return [address, privateHex, publicHex, derivationPath]; return [address, privateHex, publicHex, derivationPath];
}; };
export const generateRandomBytes = (numBytes: number): Uint8Array => {
return getRandomBytesSync(numBytes);
};
/** /**
* *
* *

115
src/libs/didPeer.ts

@ -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;
} }

2
src/main.ts

@ -154,7 +154,7 @@ function setupGlobalErrorHandler(app: VueApp) {
info: string, info: string,
) => { ) => {
console.error( console.error(
"Global Error Handler. Info:", "Ouch! Global Error Handler. Info:",
info, info,
"Error:", "Error:",
err, err,

37
src/views/StartView.vue

@ -63,14 +63,18 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { accountsDB } from "@/db/index"; import { accountsDB } from "@/db/index";
import { createJwt, registerCredential } from "@/libs/didPeer"; import { generateRandomBytes } from "@/libs/crypto";
import { createJwt, JWK, registerCredential, verifyJwt } from "@/libs/didPeer";
@Component({ @Component({
components: {}, components: {},
}) })
export default class StartView extends Vue { export default class StartView extends Vue {
numAccounts = 0; numAccounts = 0;
publicKey?: JWK;
userId?: Uint8Array;
async mounted() { async mounted() {
await accountsDB.open(); await accountsDB.open();
@ -78,22 +82,35 @@ export default class StartView extends Vue {
} }
public async onClickYes() { public async onClickYes() {
const cred = await registerCredential(); this.userId = generateRandomBytes(16);
console.log("cred", cred); const encodedUserId = new TextDecoder("utf-8").decode(this.userId);
console.log("encodedUserId", encodedUserId);
const credArrayBuffer = new TextEncoder().encode("some-random-id").buffer;
const jwt = await createJwt({ a: 1 }, "issuerDid", credArrayBuffer); const cred = await registerCredential(this.userId, generateRandomBytes(32));
console.log("jwt", jwt); console.log("public key", cred);
this.publicKey = cred.publicKey;
this.userId = cred.rawId as Uint8Array;
//this.$router.push({ name: "new-identifier" }); //this.$router.push({ name: "new-identifier" });
} }
public onClickNo() { public async onClickNo() {
this.$router.push({ name: "import-account" }); const credArrBuff = this.userId;
const jwt = await createJwt({ a: 1 }, "issuerDid", credArrBuff);
console.log("jwt", jwt);
const jwt4url = jwt
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
console.log("jwt4url", jwt4url);
const decoded = await verifyJwt(jwt, this.publicKey as JWK);
console.log("decoded", decoded);
//this.$router.push({ name: "import-account" });
} }
public onClickDerive() { public onClickDerive() {
this.$router.push({ name: "import-derive" }); //this.$router.push({ name: "import-derive" });
} }
} }
</script> </script>

Loading…
Cancel
Save