finally get something other that simplewebauthn working
This commit is contained in:
61
package-lock.json
generated
61
package-lock.json
generated
@@ -14,12 +14,12 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
|
"@peculiar/asn1-ecc": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.0",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
"@veramo/credential-w3c": "^5.6.0",
|
"@veramo/credential-w3c": "^5.6.0",
|
||||||
"@veramo/data-store": "^5.6.0",
|
"@veramo/data-store": "^5.6.0",
|
||||||
@@ -69,7 +69,9 @@
|
|||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/ramda": "^0.29.11",
|
"@types/ramda": "^0.29.11",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
@@ -6009,21 +6011,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/curves": {
|
"node_modules/@noble/curves": {
|
||||||
"version": "1.2.0",
|
"version": "1.4.0",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "1.3.2"
|
"@noble/hashes": "1.4.0"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@noble/curves/node_modules/@noble/hashes": {
|
|
||||||
"version": "1.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
|
||||||
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 16"
|
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
@@ -6106,7 +6098,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@peculiar/asn1-ecc": {
|
"node_modules/@peculiar/asn1-ecc": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@peculiar/asn1-schema": "^2.3.8",
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@peculiar/asn1-x509": "^2.3.8",
|
"@peculiar/asn1-x509": "^2.3.8",
|
||||||
@@ -6126,7 +6119,8 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@peculiar/asn1-schema": {
|
"node_modules/@peculiar/asn1-schema": {
|
||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"license": "MIT",
|
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"asn1js": "^3.0.5",
|
"asn1js": "^3.0.5",
|
||||||
"pvtsutils": "^1.3.5",
|
"pvtsutils": "^1.3.5",
|
||||||
@@ -8752,6 +8746,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/js-yaml": {
|
"node_modules/@types/js-yaml": {
|
||||||
"version": "4.0.9",
|
"version": "4.0.9",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
@@ -8768,6 +8763,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.4.2",
|
"version": "3.4.2",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
@@ -10601,11 +10597,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -12741,6 +12738,17 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ethers/node_modules/@noble/curves": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "1.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ethers/node_modules/@noble/hashes": {
|
"node_modules/ethers/node_modules/@noble/hashes": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
|
||||||
@@ -13455,9 +13463,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -14614,8 +14623,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/is-number": {
|
"node_modules/is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
@@ -20676,8 +20686,9 @@
|
|||||||
},
|
},
|
||||||
"node_modules/to-regex-range": {
|
"node_modules/to-regex-range": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
|
"@peculiar/asn1-ecc": "^2.3.8",
|
||||||
|
"@peculiar/asn1-schema": "^2.3.8",
|
||||||
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
"@pvermeer/dexie-encrypted-addon": "^3.0.0",
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "^10.0.0",
|
||||||
"@simplewebauthn/server": "^10.0.0",
|
"@simplewebauthn/server": "^10.0.0",
|
||||||
"@tweenjs/tween.js": "^21.1.1",
|
"@tweenjs/tween.js": "^21.1.1",
|
||||||
"@types/js-yaml": "^4.0.9",
|
|
||||||
"@types/luxon": "^3.4.2",
|
|
||||||
"@veramo/core": "^5.6.0",
|
"@veramo/core": "^5.6.0",
|
||||||
"@veramo/credential-w3c": "^5.6.0",
|
"@veramo/credential-w3c": "^5.6.0",
|
||||||
"@veramo/data-store": "^5.6.0",
|
"@veramo/data-store": "^5.6.0",
|
||||||
@@ -71,7 +71,9 @@
|
|||||||
"web-did-resolver": "^2.0.27"
|
"web-did-resolver": "^2.0.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/ramda": "^0.29.11",
|
"@types/ramda": "^0.29.11",
|
||||||
"@types/three": "^0.155.1",
|
"@types/three": "^0.155.1",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
|||||||
102
src/libs/crypto/passkeyHelpers.ts
Normal file
102
src/libs/crypto/passkeyHelpers.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/unwrapEC2Signature.ts
|
||||||
|
import { AsnParser } from "@peculiar/asn1-schema";
|
||||||
|
import { ECDSASigValue } from "@peculiar/asn1-ecc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In WebAuthn, EC2 signatures are wrapped in ASN.1 structure so we need to peel r and s apart.
|
||||||
|
*
|
||||||
|
* See https://www.w3.org/TR/webauthn-2/#sctn-signature-attestation-types
|
||||||
|
*/
|
||||||
|
export function unwrapEC2Signature(signature: Uint8Array): Uint8Array {
|
||||||
|
const parsedSignature = AsnParser.parse(signature, ECDSASigValue);
|
||||||
|
let rBytes = new Uint8Array(parsedSignature.r);
|
||||||
|
let sBytes = new Uint8Array(parsedSignature.s);
|
||||||
|
|
||||||
|
if (shouldRemoveLeadingZero(rBytes)) {
|
||||||
|
rBytes = rBytes.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRemoveLeadingZero(sBytes)) {
|
||||||
|
sBytes = sBytes.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalSignature = isoUint8ArrayConcat([rBytes, sBytes]);
|
||||||
|
|
||||||
|
return finalSignature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the DER-specific `00` byte at the start of an ECDSA signature byte sequence
|
||||||
|
* should be removed based on the following logic:
|
||||||
|
*
|
||||||
|
* "If the leading byte is 0x0, and the the high order bit on the second byte is not set to 0,
|
||||||
|
* then remove the leading 0x0 byte"
|
||||||
|
*/
|
||||||
|
function shouldRemoveLeadingZero(bytes: Uint8Array): boolean {
|
||||||
|
return bytes[0] === 0x0 && (bytes[1] & (1 << 7)) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoUint8Array.ts#L49
|
||||||
|
/**
|
||||||
|
* Combine multiple Uint8Arrays into a single Uint8Array
|
||||||
|
*/
|
||||||
|
export function isoUint8ArrayConcat(arrays: Uint8Array[]): Uint8Array {
|
||||||
|
let pointer = 0;
|
||||||
|
const totalLength = arrays.reduce((prev, curr) => prev + curr.length, 0);
|
||||||
|
|
||||||
|
const toReturn = new Uint8Array(totalLength);
|
||||||
|
|
||||||
|
arrays.forEach((arr) => {
|
||||||
|
toReturn.set(arr, pointer);
|
||||||
|
pointer += arr.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
return toReturn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// from https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/iso/isoCrypto/getWebCrypto.ts
|
||||||
|
let webCrypto: unknown = undefined;
|
||||||
|
export function getWebCrypto() {
|
||||||
|
/**
|
||||||
|
* Hello there! If you came here wondering why this method is asynchronous when use of
|
||||||
|
* `globalThis.crypto` is not, it's to minimize a bunch of refactor related to making this
|
||||||
|
* synchronous. For example, `generateRegistrationOptions()` and `generateAuthenticationOptions()`
|
||||||
|
* become synchronous if we make this synchronous (since nothing else in that method is async)
|
||||||
|
* which represents a breaking API change in this library's core API.
|
||||||
|
*
|
||||||
|
* TODO: If it's after February 2025 when you read this then consider whether it still makes sense
|
||||||
|
* to keep this method asynchronous.
|
||||||
|
*/
|
||||||
|
const toResolve = new Promise((resolve, reject) => {
|
||||||
|
if (webCrypto) {
|
||||||
|
return resolve(webCrypto);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Naively attempt to access Crypto as a global object, which popular ESM-centric run-times
|
||||||
|
* support (and Node v20+)
|
||||||
|
*/
|
||||||
|
const _globalThisCrypto = _getWebCryptoInternals.stubThisGlobalThisCrypto();
|
||||||
|
if (_globalThisCrypto) {
|
||||||
|
webCrypto = _globalThisCrypto;
|
||||||
|
return resolve(webCrypto);
|
||||||
|
}
|
||||||
|
// We tried to access it both in Node and globally, so bail out
|
||||||
|
return reject(new MissingWebCrypto());
|
||||||
|
});
|
||||||
|
return toResolve;
|
||||||
|
}
|
||||||
|
export class MissingWebCrypto extends Error {
|
||||||
|
constructor() {
|
||||||
|
const message = "An instance of the Crypto API could not be located";
|
||||||
|
super(message);
|
||||||
|
this.name = "MissingWebCrypto";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Make it possible to stub return values during testing
|
||||||
|
export const _getWebCryptoInternals = {
|
||||||
|
stubThisGlobalThisCrypto: () => globalThis.crypto,
|
||||||
|
// Make it possible to reset the `webCrypto` at the top of the file
|
||||||
|
setCachedCrypto: (newCrypto: unknown) => {
|
||||||
|
webCrypto = newCrypto;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import asn1 from "asn1-ber";
|
import asn1 from "asn1-ber";
|
||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import { decode as cborDecode } from "cbor-x";
|
import { decode as cborDecode } from "cbor-x";
|
||||||
import { createJWS, JWTPayload, verifyJWT } from "did-jwt";
|
import { createJWS, JWTPayload } from "did-jwt";
|
||||||
import { DIDResolutionResult, Resolver } from "did-resolver";
|
import { DIDResolutionResult } from "did-resolver";
|
||||||
import { sha256 } from "ethereum-cryptography/sha256.js";
|
import { sha256 } from "ethereum-cryptography/sha256.js";
|
||||||
import { bytesToMultibase } from "@veramo/utils";
|
import { bytesToMultibase } from "@veramo/utils";
|
||||||
import {
|
import {
|
||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
} from "@simplewebauthn/types";
|
} from "@simplewebauthn/types";
|
||||||
|
|
||||||
import { generateRandomBytes } from "@/libs/crypto";
|
import { generateRandomBytes } from "@/libs/crypto";
|
||||||
|
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers";
|
||||||
|
|
||||||
export interface JWK {
|
export interface JWK {
|
||||||
kty: string;
|
kty: string;
|
||||||
@@ -77,12 +78,24 @@ export async function registerCredential(userId: Uint8Array) {
|
|||||||
expectedRPID: window.location.hostname,
|
expectedRPID: window.location.hostname,
|
||||||
});
|
});
|
||||||
console.log("verification", verification);
|
console.log("verification", verification);
|
||||||
|
const jwkObj = cborDecode(
|
||||||
|
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||||
|
);
|
||||||
|
console.log("jwkObj from verification", jwkObj);
|
||||||
|
console.log(
|
||||||
|
"[1]==2 => kty EC",
|
||||||
|
"[3]==-7 => alg ES256",
|
||||||
|
"[-1]==1 => crv P-256",
|
||||||
|
);
|
||||||
|
const { publicKeyJwk } = cborToKeys(
|
||||||
|
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authData: verification.registrationInfo?.attestationObject,
|
authData: verification.registrationInfo?.attestationObject,
|
||||||
credId: verification.registrationInfo?.credentialID as string,
|
credId: verification.registrationInfo?.credentialID as string,
|
||||||
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")),
|
rawId: new Uint8Array(new Buffer(attResp.rawId, "base64")),
|
||||||
publicKeyJwk: undefined,
|
publicKeyJwk: publicKeyJwk,
|
||||||
publicKeyBytes: verification.registrationInfo
|
publicKeyBytes: verification.registrationInfo
|
||||||
?.credentialPublicKey as Uint8Array,
|
?.credentialPublicKey as Uint8Array,
|
||||||
};
|
};
|
||||||
@@ -153,30 +166,9 @@ export async function registerCredential2(userId: Uint8Array) {
|
|||||||
);
|
);
|
||||||
console.log("attestationObject", attestationObject);
|
console.log("attestationObject", attestationObject);
|
||||||
|
|
||||||
const jwkObj = cborDecode(
|
const { publicKeyJwk, publicKeyBuffer } = cborToKeys(
|
||||||
verification.registrationInfo?.credentialPublicKey as Uint8Array,
|
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: arrayToBase64Url(jwkObj[-2]),
|
|
||||||
y: arrayToBase64Url(jwkObj[-3]),
|
|
||||||
};
|
|
||||||
const publicKeyBytes = Buffer.concat([
|
|
||||||
Buffer.from(jwkObj[-2]),
|
|
||||||
Buffer.from(jwkObj[-3]),
|
|
||||||
]);
|
|
||||||
|
|
||||||
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
//const publicKeyBytes = extractPublicKeyCose(attestationObject.authData);
|
||||||
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
//const publicKeyJwk = extractPublicKeyJwk(attestationObject.authData);
|
||||||
@@ -186,7 +178,7 @@ export async function registerCredential2(userId: Uint8Array) {
|
|||||||
credId: credential?.id,
|
credId: credential?.id,
|
||||||
rawId: credential?.rawId,
|
rawId: credential?.rawId,
|
||||||
publicKeyJwk,
|
publicKeyJwk,
|
||||||
publicKeyBytes,
|
publicKeyBytes: publicKeyBuffer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,9 +200,12 @@ export function createPeerDid(publicKeyBytes: Uint8Array) {
|
|||||||
|
|
||||||
export class PeerSetup {
|
export class PeerSetup {
|
||||||
public authenticatorData?: ArrayBuffer;
|
public authenticatorData?: ArrayBuffer;
|
||||||
|
public authenticatorDataBase64Url?: Base64URLString;
|
||||||
public challenge?: Uint8Array;
|
public challenge?: Uint8Array;
|
||||||
public clientDataJsonDecoded?: object;
|
public clientDataJsonDecoded?: object;
|
||||||
|
public clientDataJsonBase64Url?: Base64URLString;
|
||||||
public signature?: Base64URLString;
|
public signature?: Base64URLString;
|
||||||
|
public publicKeyJwk?: JWK;
|
||||||
|
|
||||||
public async createJwt(
|
public async createJwt(
|
||||||
payload: object,
|
payload: object,
|
||||||
@@ -244,16 +239,23 @@ export class PeerSetup {
|
|||||||
const clientAuth = await startAuthentication(options);
|
const clientAuth = await startAuthentication(options);
|
||||||
console.log("custom clientAuth", clientAuth);
|
console.log("custom clientAuth", clientAuth);
|
||||||
|
|
||||||
|
this.authenticatorDataBase64Url = clientAuth.response.authenticatorData;
|
||||||
this.authenticatorData = Buffer.from(
|
this.authenticatorData = Buffer.from(
|
||||||
clientAuth.response.authenticatorData,
|
clientAuth.response.authenticatorData,
|
||||||
"base64",
|
"base64",
|
||||||
).buffer;
|
).buffer;
|
||||||
this.challenge = payloadHash;
|
this.challenge = payloadHash;
|
||||||
|
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON;
|
||||||
this.clientDataJsonDecoded = JSON.parse(
|
this.clientDataJsonDecoded = JSON.parse(
|
||||||
Buffer.from(clientAuth.response.clientDataJSON, "base64").toString(
|
Buffer.from(clientAuth.response.clientDataJSON, "base64").toString(
|
||||||
"utf-8",
|
"utf-8",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
console.log("authenticatorData for signing", this.authenticatorData);
|
||||||
|
console.log(
|
||||||
|
"clientDataJSON for signing",
|
||||||
|
Buffer.from(clientAuth.response.clientDataJSON, "base64"),
|
||||||
|
);
|
||||||
this.signature = clientAuth.response.signature;
|
this.signature = clientAuth.response.signature;
|
||||||
|
|
||||||
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64");
|
const headerBase64 = Buffer.from(JSON.stringify(header)).toString("base64");
|
||||||
@@ -373,9 +375,12 @@ export async function verifyJwt(
|
|||||||
credId: Base64URLString,
|
credId: Base64URLString,
|
||||||
rawId: Uint8Array,
|
rawId: Uint8Array,
|
||||||
authenticatorData: ArrayBuffer,
|
authenticatorData: ArrayBuffer,
|
||||||
|
authenticatorDataBase64Url: Base64URLString,
|
||||||
challenge: Uint8Array,
|
challenge: Uint8Array,
|
||||||
clientDataJSON: object,
|
clientDataJSON: object,
|
||||||
publicKey: Uint8Array,
|
clientDataJsonBase64Url: Base64URLString,
|
||||||
|
publicKeyBytes: Uint8Array,
|
||||||
|
publicKeyJwk: JWK,
|
||||||
signature: Base64URLString,
|
signature: Base64URLString,
|
||||||
) {
|
) {
|
||||||
// Here's a combined auth & verify process, based on some of the inputs.
|
// Here's a combined auth & verify process, based on some of the inputs.
|
||||||
@@ -397,7 +402,7 @@ export async function verifyJwt(
|
|||||||
// const verfOpts: VerifyAuthenticationResponseOpts = {
|
// const verfOpts: VerifyAuthenticationResponseOpts = {
|
||||||
// authenticator: {
|
// authenticator: {
|
||||||
// credentialID: credId,
|
// credentialID: credId,
|
||||||
// credentialPublicKey: publicKey,
|
// credentialPublicKey: publicKeyBytes,
|
||||||
// counter: 0,
|
// counter: 0,
|
||||||
// },
|
// },
|
||||||
// expectedChallenge: options.challenge,
|
// expectedChallenge: options.challenge,
|
||||||
@@ -413,7 +418,7 @@ export async function verifyJwt(
|
|||||||
const authOpts: VerifyAuthenticationResponseOpts = {
|
const authOpts: VerifyAuthenticationResponseOpts = {
|
||||||
authenticator: {
|
authenticator: {
|
||||||
credentialID: credId,
|
credentialID: credId,
|
||||||
credentialPublicKey: publicKey,
|
credentialPublicKey: publicKeyBytes,
|
||||||
counter: 0,
|
counter: 0,
|
||||||
},
|
},
|
||||||
expectedChallenge: arrayToBase64Url(challenge),
|
expectedChallenge: arrayToBase64Url(challenge),
|
||||||
@@ -438,10 +443,107 @@ export async function verifyJwt(
|
|||||||
const verification = await verifyAuthenticationResponse(authOpts);
|
const verification = await verifyAuthenticationResponse(authOpts);
|
||||||
console.log("auth verification", verification);
|
console.log("auth verification", verification);
|
||||||
|
|
||||||
const decoded = verifyJWT(jwt, {
|
// It doesn't work to use the did-jwt verifyJWT with did-resolver Resolver
|
||||||
resolver: new Resolver({ peer: peerDidToDidDocument }),
|
// const decoded = verifyJWT(jwt, {
|
||||||
});
|
// resolver: new Resolver({ peer: peerDidToDidDocument }),
|
||||||
return decoded;
|
// });
|
||||||
|
// return decoded;
|
||||||
|
|
||||||
|
// const [headerB64, concatenatedPayloadB64, signatureB64] = jwt.split(".");
|
||||||
|
//
|
||||||
|
// const header = JSON.parse(atob(headerB64));
|
||||||
|
// const jsonPayload = JSON.parse(atob(concatenatedPayloadB64));
|
||||||
|
// const [jsonPayloadB64, otherDataHashB64] = concatenatedPayloadB64.split(".");
|
||||||
|
//
|
||||||
|
// const otherDataHash = base64urlDecode(otherDataHashB64);
|
||||||
|
// const signature = base64urlDecode(signatureB64);
|
||||||
|
//
|
||||||
|
// const dataToVerify = new TextEncoder().encode(`${headerB64}.${concatenatedPayloadB64}`);
|
||||||
|
// const dataHash = await sha256(dataToVerify);
|
||||||
|
//
|
||||||
|
// const authenticatorData = base64urlDecode(jsonPayload.authenticatorData);
|
||||||
|
// const clientDataJSON = jsonPayload.clientDataJSON;
|
||||||
|
|
||||||
|
/////////
|
||||||
|
|
||||||
|
// const clientBuffer = clientDataJsonBase64Url
|
||||||
|
// .replace(/-/g, "+")
|
||||||
|
// .replace(/_/g, "/");
|
||||||
|
// const clientData = new Uint8Array(Buffer.from(clientBuffer, "base64"));
|
||||||
|
// const clientDataHash = sha256(clientData);
|
||||||
|
//
|
||||||
|
// const verifyData = new Uint8Array([
|
||||||
|
// ...new Uint8Array(authenticatorData),
|
||||||
|
// ...new Uint8Array(clientDataHash),
|
||||||
|
// ]);
|
||||||
|
// console.log("verifyData", verifyData);
|
||||||
|
// const sigBase64Raw = signature.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
//
|
||||||
|
// const sigHex = new Uint8Array(Buffer.from(sigBase64Raw, "base64")); //Buffer.from(sigBase64Raw, "base64").toString("hex");
|
||||||
|
// const msgHex = verifyData; //new Buffer(verifyData).toString("hex");
|
||||||
|
// const pubHex = publicKeyBytes; //new Buffer(publicKeyBytes).toString("hex");
|
||||||
|
// console.log("sig msg pub", sigHex, msgHex, pubHex);
|
||||||
|
// const isValid = p256.verify(sigHex, msgHex, pubHex);
|
||||||
|
|
||||||
|
/////////
|
||||||
|
|
||||||
|
const authDataFromBase = Buffer.from(authenticatorDataBase64Url, "base64");
|
||||||
|
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64");
|
||||||
|
const sigBuffer = Buffer.from(signature, "base64");
|
||||||
|
const finalSigBuffer = unwrapEC2Signature(sigBuffer);
|
||||||
|
|
||||||
|
// Hash the client data
|
||||||
|
const hash = sha256(clientDataFromBase);
|
||||||
|
|
||||||
|
// Construct the preimage
|
||||||
|
const preimage = Buffer.concat([authDataFromBase, hash]);
|
||||||
|
|
||||||
|
console.log("finalSigBuffer", finalSigBuffer);
|
||||||
|
console.log("preimage", preimage);
|
||||||
|
console.log("publicKeyBytes", publicKeyBytes);
|
||||||
|
|
||||||
|
// This uses p256 from @noble/curves/p256, which I would prefer but it's returning false.
|
||||||
|
// const isValid = p256.verify(
|
||||||
|
// finalSigBuffer,
|
||||||
|
// new Uint8Array(preimage),
|
||||||
|
// publicKeyBytes,
|
||||||
|
// );
|
||||||
|
// console.log("isValid", isValid);
|
||||||
|
|
||||||
|
/////////
|
||||||
|
|
||||||
|
const WebCrypto = await getWebCrypto();
|
||||||
|
const verifyAlgorithm = {
|
||||||
|
name: "ECDSA",
|
||||||
|
hash: { name: "SHA-256" },
|
||||||
|
};
|
||||||
|
const keyAlgorithm = {
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: publicKeyJwk.crv,
|
||||||
|
};
|
||||||
|
// const publicKeyCryptoKey = await importKey({
|
||||||
|
// publicKeyJwk,
|
||||||
|
// algorithm: keyAlgorithm,
|
||||||
|
// });
|
||||||
|
const publicKeyCryptoKey = await WebCrypto.subtle.importKey(
|
||||||
|
"jwk",
|
||||||
|
publicKeyJwk,
|
||||||
|
keyAlgorithm,
|
||||||
|
false,
|
||||||
|
["verify"],
|
||||||
|
);
|
||||||
|
console.log("verifyAlgorithm", verifyAlgorithm);
|
||||||
|
console.log("publicKeyCryptoKey", publicKeyCryptoKey);
|
||||||
|
console.log("finalSigBuffer", finalSigBuffer);
|
||||||
|
console.log("preimage", preimage);
|
||||||
|
const verified = await WebCrypto.subtle.verify(
|
||||||
|
verifyAlgorithm,
|
||||||
|
publicKeyCryptoKey,
|
||||||
|
finalSigBuffer,
|
||||||
|
preimage,
|
||||||
|
);
|
||||||
|
console.log("verified", verified);
|
||||||
|
return verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
||||||
@@ -481,3 +583,86 @@ async function peerDidToDidDocument(did: string): Promise<DIDResolutionResult> {
|
|||||||
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
didResolutionMetadata: { contentType: "application/did+ld+json" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert COSE public key to PEM format
|
||||||
|
function COSEtoPEM(cose: Buffer) {
|
||||||
|
// const alg = cose.get(3); // Algorithm
|
||||||
|
const x = cose[-2]; // x-coordinate
|
||||||
|
const y = cose[-3]; // y-coordinate
|
||||||
|
|
||||||
|
// Ensure the coordinates are in the correct format
|
||||||
|
const pubKeyBuffer = Buffer.concat([Buffer.from([0x04]), x, y]);
|
||||||
|
|
||||||
|
// Convert to PEM format
|
||||||
|
const pem = `-----BEGIN PUBLIC KEY-----
|
||||||
|
${pubKeyBuffer.toString("base64")}
|
||||||
|
-----END PUBLIC KEY-----`;
|
||||||
|
|
||||||
|
return pem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64urlDecode(input: string) {
|
||||||
|
input = input.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
const pad = input.length % 4 === 0 ? "" : "====".slice(input.length % 4);
|
||||||
|
const str = atob(input + pad);
|
||||||
|
const bytes = new Uint8Array(str.length);
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
bytes[i] = str.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return bytes.buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64urlEncode(buffer: ArrayBuffer) {
|
||||||
|
const str = String.fromCharCode(...new Uint8Array(buffer));
|
||||||
|
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function cborToKeys(publicKeyBytes: Uint8Array) {
|
||||||
|
const jwkObj = cborDecode(publicKeyBytes);
|
||||||
|
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: arrayToBase64Url(jwkObj[-2]),
|
||||||
|
y: arrayToBase64Url(jwkObj[-3]),
|
||||||
|
};
|
||||||
|
const publicKeyBuffer = Buffer.concat([
|
||||||
|
Buffer.from(jwkObj[-2]),
|
||||||
|
Buffer.from(jwkObj[-3]),
|
||||||
|
]);
|
||||||
|
return { publicKeyJwk, publicKeyBuffer };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pemToCryptoKey(pem: string) {
|
||||||
|
const binaryDerString = atob(
|
||||||
|
pem
|
||||||
|
.split("\n")
|
||||||
|
.filter((x) => !x.includes("-----"))
|
||||||
|
.join(""),
|
||||||
|
);
|
||||||
|
const binaryDer = new Uint8Array(binaryDerString.length);
|
||||||
|
for (let i = 0; i < binaryDerString.length; i++) {
|
||||||
|
binaryDer[i] = binaryDerString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
console.log("binaryDer", binaryDer.buffer);
|
||||||
|
return await window.crypto.subtle.importKey(
|
||||||
|
"spki",
|
||||||
|
binaryDer.buffer,
|
||||||
|
{
|
||||||
|
name: "RSASSA-PKCS1-v1_5",
|
||||||
|
hash: "SHA-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["verify"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -265,9 +265,7 @@ export default class Help extends Vue {
|
|||||||
const encodedUserId = Buffer.from(this.userId).toString("base64");
|
const encodedUserId = Buffer.from(this.userId).toString("base64");
|
||||||
console.log("encodedUserId", encodedUserId);
|
console.log("encodedUserId", encodedUserId);
|
||||||
|
|
||||||
const cred = await registerCredential(
|
const cred = await registerCredential(this.userId as Uint8Array);
|
||||||
this.userId as Uint8Array
|
|
||||||
);
|
|
||||||
console.log("public key", cred);
|
console.log("public key", cred);
|
||||||
this.publicKeyJwk = cred.publicKeyJwk;
|
this.publicKeyJwk = cred.publicKeyJwk;
|
||||||
this.publicKeyBytes = cred.publicKeyBytes;
|
this.publicKeyBytes = cred.publicKeyBytes;
|
||||||
@@ -277,6 +275,7 @@ export default class Help extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async create() {
|
public async create() {
|
||||||
|
console.log("Starting a create");
|
||||||
const did = createPeerDid(this.publicKeyBytes as Uint8Array);
|
const did = createPeerDid(this.publicKeyBytes as Uint8Array);
|
||||||
console.log("did", did);
|
console.log("did", did);
|
||||||
this.peerSetup = new PeerSetup();
|
this.peerSetup = new PeerSetup();
|
||||||
@@ -303,9 +302,12 @@ export default class Help extends Vue {
|
|||||||
this.credId as Base64URLString,
|
this.credId as Base64URLString,
|
||||||
this.rawId as Uint8Array,
|
this.rawId as Uint8Array,
|
||||||
this.peerSetup.authenticatorData as ArrayBuffer,
|
this.peerSetup.authenticatorData as ArrayBuffer,
|
||||||
|
this.peerSetup.authenticatorDataBase64Url as Base64URLString,
|
||||||
this.peerSetup.challenge as Uint8Array,
|
this.peerSetup.challenge as Uint8Array,
|
||||||
this.peerSetup.clientDataJsonDecoded,
|
this.peerSetup.clientDataJsonDecoded,
|
||||||
|
this.peerSetup.clientDataJsonBase64Url as Base64URLString,
|
||||||
this.publicKeyBytes as Uint8Array,
|
this.publicKeyBytes as Uint8Array,
|
||||||
|
this.publicKeyJwk as JWK,
|
||||||
this.peerSetup.signature as Base64URLString,
|
this.peerSetup.signature as Base64URLString,
|
||||||
);
|
);
|
||||||
console.log("decoded", decoded);
|
console.log("decoded", decoded);
|
||||||
|
|||||||
Reference in New Issue
Block a user