11 changed files with 1837 additions and 1475 deletions
File diff suppressed because it is too large
@ -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; |
|||
}, |
|||
}; |
@ -0,0 +1,570 @@ |
|||
import asn1 from "asn1-ber"; |
|||
import { Buffer } from "buffer/"; |
|||
import { decode as cborDecode } from "cbor-x"; |
|||
import { bytesToMultibase, JWTPayload, multibaseToBytes } from "did-jwt"; |
|||
import { DIDResolutionResult } from "did-resolver"; |
|||
import { sha256 } from "ethereum-cryptography/sha256.js"; |
|||
import { |
|||
startAuthentication, |
|||
startRegistration, |
|||
} from "@simplewebauthn/browser"; |
|||
import { |
|||
generateAuthenticationOptions, |
|||
generateRegistrationOptions, |
|||
verifyAuthenticationResponse, |
|||
verifyRegistrationResponse, |
|||
} from "@simplewebauthn/server"; |
|||
import { VerifyAuthenticationResponseOpts } from "@simplewebauthn/server/esm/authentication/verifyAuthenticationResponse"; |
|||
import { |
|||
Base64URLString, |
|||
PublicKeyCredentialCreationOptionsJSON, |
|||
PublicKeyCredentialRequestOptionsJSON, |
|||
} from "@simplewebauthn/types"; |
|||
|
|||
import { getWebCrypto, unwrapEC2Signature } from "@/libs/crypto/passkeyHelpers"; |
|||
|
|||
const PEER_DID_PREFIX = "did:peer:"; |
|||
const PEER_DID_MULTIBASE_PREFIX = PEER_DID_PREFIX + "0"; |
|||
export interface JWK { |
|||
kty: string; |
|||
crv: string; |
|||
x: string; |
|||
y: string; |
|||
} |
|||
|
|||
function toBase64Url(anythingB64: string) { |
|||
return anythingB64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); |
|||
} |
|||
|
|||
function arrayToBase64Url(anything: Uint8Array) { |
|||
return toBase64Url(Buffer.from(anything).toString("base64")); |
|||
} |
|||
|
|||
export async function registerCredential(passkeyName?: string) { |
|||
const options: PublicKeyCredentialCreationOptionsJSON = |
|||
await generateRegistrationOptions({ |
|||
rpName: "Time Safari", |
|||
rpID: window.location.hostname, |
|||
userName: passkeyName || "Time Safari User", |
|||
// Don't prompt users for additional information about the authenticator
|
|||
// (Recommended for smoother UX)
|
|||
attestationType: "none", |
|||
authenticatorSelection: { |
|||
// Defaults
|
|||
residentKey: "preferred", |
|||
userVerification: "preferred", |
|||
// Optional
|
|||
authenticatorAttachment: "platform", |
|||
}, |
|||
}); |
|||
// someday, instead of simplwebauthn, we'll go direct: navigator.credentials.create with PublicKeyCredentialCreationOptions
|
|||
// with pubKeyCredParams: { type: "public-key", alg: -7 }
|
|||
const attResp = await startRegistration(options); |
|||
const verification = await verifyRegistrationResponse({ |
|||
response: attResp, |
|||
expectedChallenge: options.challenge, |
|||
expectedOrigin: window.location.origin, |
|||
expectedRPID: window.location.hostname, |
|||
}); |
|||
|
|||
// references for parsing auth data and getting the public key
|
|||
// https://github.com/MasterKale/SimpleWebAuthn/blob/master/packages/server/src/helpers/parseAuthenticatorData.ts#L11
|
|||
// https://chatgpt.com/share/78a5c91d-099d-46dc-aa6d-fc0c916509fa
|
|||
// https://chatgpt.com/share/3c13f061-6031-45bc-a2d7-3347c1e7a2d7
|
|||
|
|||
const credIdBase64Url = verification.registrationInfo?.credentialID as string; |
|||
if (attResp.rawId !== credIdBase64Url) { |
|||
console.log("Warning! The raw ID does not match the credential ID.") |
|||
} |
|||
const credIdHex = Buffer.from( |
|||
base64URLStringToArrayBuffer(credIdBase64Url), |
|||
).toString("hex"); |
|||
const { publicKeyJwk } = cborToKeys( |
|||
verification.registrationInfo?.credentialPublicKey as Uint8Array, |
|||
); |
|||
|
|||
return { |
|||
authData: verification.registrationInfo?.attestationObject, |
|||
credIdHex: credIdHex, |
|||
publicKeyJwk: publicKeyJwk, |
|||
publicKeyBytes: verification.registrationInfo |
|||
?.credentialPublicKey as 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( |
|||
publicKeyBytes, |
|||
"base58btc", |
|||
"p256-pub", |
|||
); |
|||
return PEER_DID_MULTIBASE_PREFIX + methodSpecificId; |
|||
} |
|||
|
|||
function peerDidToPublicKeyBytes(did: string) { |
|||
return multibaseToBytes(did.substring(PEER_DID_MULTIBASE_PREFIX.length)); |
|||
} |
|||
|
|||
export class PeerSetup { |
|||
public authenticatorData?: ArrayBuffer; |
|||
public challenge?: Uint8Array; |
|||
public clientDataJsonBase64Url?: Base64URLString; |
|||
public signature?: Base64URLString; |
|||
|
|||
public async createJwtSimplewebauthn( |
|||
issuerDid: string, |
|||
payload: object, |
|||
credIdHex: string, |
|||
) { |
|||
const credentialId = arrayBufferToBase64URLString( |
|||
Buffer.from(credIdHex, "hex").buffer, |
|||
); |
|||
const fullPayload = { |
|||
...payload, |
|||
iat: Math.floor(Date.now() / 1000), |
|||
iss: issuerDid, |
|||
}; |
|||
this.challenge = new Uint8Array(Buffer.from(JSON.stringify(fullPayload))); |
|||
// const payloadHash: Uint8Array = sha256(this.challenge);
|
|||
const options: PublicKeyCredentialRequestOptionsJSON = |
|||
await generateAuthenticationOptions({ |
|||
challenge: this.challenge, |
|||
rpID: window.location.hostname, |
|||
allowCredentials: [{ id: credentialId }], |
|||
}); |
|||
// console.log("simple authentication options", options);
|
|||
|
|||
const clientAuth = await startAuthentication(options); |
|||
// console.log("simple credential get", clientAuth);
|
|||
|
|||
const authenticatorDataBase64Url = clientAuth.response.authenticatorData; |
|||
this.authenticatorData = Buffer.from( |
|||
clientAuth.response.authenticatorData, |
|||
"base64", |
|||
).buffer; |
|||
this.clientDataJsonBase64Url = clientAuth.response.clientDataJSON; |
|||
// console.log("simple authenticatorData for signing", this.authenticatorData);
|
|||
this.signature = clientAuth.response.signature; |
|||
|
|||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
|||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; |
|||
const headerBase64 = Buffer.from(JSON.stringify(header)) |
|||
.toString("base64") |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_") |
|||
.replace(/=+$/, ""); |
|||
|
|||
const dataInJwt = { |
|||
AuthenticationDataB64URL: authenticatorDataBase64Url, |
|||
ClientDataJSONB64URL: this.clientDataJsonBase64Url, |
|||
iat: Math.floor(Date.now() / 1000), |
|||
iss: issuerDid, |
|||
}; |
|||
const dataInJwtString = JSON.stringify(dataInJwt); |
|||
const payloadBase64 = Buffer.from(dataInJwtString) |
|||
.toString("base64") |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_") |
|||
.replace(/=+$/, ""); |
|||
|
|||
const signature = clientAuth.response.signature; |
|||
|
|||
return headerBase64 + "." + payloadBase64 + "." + signature; |
|||
} |
|||
|
|||
public async createJwtNavigator( |
|||
issuerDid: string, |
|||
payload: object, |
|||
credIdHex: string, |
|||
) { |
|||
const fullPayload = { |
|||
...payload, |
|||
iat: Math.floor(Date.now() / 1000), |
|||
iss: issuerDid, |
|||
}; |
|||
const dataToSignString = JSON.stringify(fullPayload); |
|||
const dataToSignBuffer = Buffer.from(dataToSignString); |
|||
const credentialId = Buffer.from(credIdHex, "hex"); |
|||
|
|||
// console.log("lower credentialId", credentialId);
|
|||
this.challenge = new Uint8Array(dataToSignBuffer); |
|||
const options = { |
|||
publicKey: { |
|||
allowCredentials: [ |
|||
{ |
|||
id: credentialId, |
|||
type: "public-key", |
|||
}, |
|||
], |
|||
challenge: this.challenge.buffer, |
|||
rpID: window.location.hostname, |
|||
userVerification: "preferred", |
|||
}, |
|||
}; |
|||
|
|||
const credential = await navigator.credentials.get(options); |
|||
// console.log("nav credential get", credential);
|
|||
|
|||
this.authenticatorData = credential?.response.authenticatorData; |
|||
const authenticatorDataBase64Url = arrayBufferToBase64URLString( |
|||
this.authenticatorData, |
|||
); |
|||
|
|||
this.clientDataJsonBase64Url = arrayBufferToBase64URLString( |
|||
credential?.response.clientDataJSON, |
|||
); |
|||
|
|||
// Our custom type of JWANT means the signature is based on a concatenation of the two Webauthn properties
|
|||
const header: JWTPayload = { typ: "JWANT", alg: "ES256" }; |
|||
const headerBase64 = Buffer.from(JSON.stringify(header)) |
|||
.toString("base64") |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_") |
|||
.replace(/=+$/, ""); |
|||
|
|||
const dataInJwt = { |
|||
AuthenticationDataB64URL: authenticatorDataBase64Url, |
|||
ClientDataJSONB64URL: this.clientDataJsonBase64Url, |
|||
iat: Math.floor(Date.now() / 1000), |
|||
iss: issuerDid, |
|||
}; |
|||
const dataInJwtString = JSON.stringify(dataInJwt); |
|||
const payloadBase64 = Buffer.from(dataInJwtString) |
|||
.toString("base64") |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_") |
|||
.replace(/=+$/, ""); |
|||
|
|||
const origSignature = Buffer.from(credential?.response.signature) |
|||
.toString("base64") |
|||
this.signature = origSignature |
|||
.replace(/\+/g, "-") |
|||
.replace(/\//g, "_") |
|||
.replace(/=+$/, ""); |
|||
|
|||
const jwt = headerBase64 + "." + payloadBase64 + "." + this.signature; |
|||
return jwt; |
|||
} |
|||
|
|||
// return a low-level signing function, similar to createJWS approach
|
|||
// async webAuthnES256KSigner(credentialID: string) {
|
|||
// return async (data: string | Uint8Array) => {
|
|||
// // get signature from WebAuthn
|
|||
// const signature = await this.generateWebAuthnSignature(data);
|
|||
//
|
|||
// // 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("lower signature inside signer", signature);
|
|||
// console.log("lower buffer signature inside signer", signatureBuffer);
|
|||
// console.log("lower base64 buffer signature inside signer", signatureBuffer.toString("base64"));
|
|||
// // Decode the DER-encoded signature to extract R and S values
|
|||
// const reader = new asn1.BerReader(signatureBuffer);
|
|||
// console.log("lower after reader");
|
|||
// reader.readSequence();
|
|||
// console.log("lower after read sequence");
|
|||
// const r = reader.readString(asn1.Ber.Integer, true);
|
|||
// console.log("lower after r");
|
|||
// const s = reader.readString(asn1.Ber.Integer, true);
|
|||
// console.log("lower after r & s");
|
|||
//
|
|||
// // Ensure R and S are 32 bytes each
|
|||
// const rBuffer = Buffer.from(r);
|
|||
// const sBuffer = Buffer.from(s);
|
|||
// console.log("lower 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;
|
|||
//
|
|||
// // Concatenate R and S to form the 64-byte array (ECDSA signature format expected by JWT)
|
|||
// const combinedSignature = Buffer.concat([rPadded, sPadded]);
|
|||
// console.log(
|
|||
// "lower combinedSignature",
|
|||
// combinedSignature.length,
|
|||
// combinedSignature,
|
|||
// );
|
|||
//
|
|||
// const combSig64 = combinedSignature.toString("base64");
|
|||
// console.log("lower combSig64", combSig64);
|
|||
// const combSig64Url = combSig64
|
|||
// .replace(/\+/g, "-")
|
|||
// .replace(/\//g, "_")
|
|||
// .replace(/=+$/, "");
|
|||
// console.log("lower combSig64Url", combSig64Url);
|
|||
// return combSig64Url;
|
|||
// };
|
|||
// }
|
|||
} |
|||
|
|||
// I'd love to use this but it doesn't verify.
|
|||
// Requires:
|
|||
// npm install @noble/curves
|
|||
// ... and this import:
|
|||
// import { p256 } from "@noble/curves/p256";
|
|||
export async function verifyJwtP256( |
|||
credIdHex: string, |
|||
issuerDid: string, |
|||
authenticatorData: ArrayBuffer, |
|||
challenge: Uint8Array, |
|||
clientDataJsonBase64Url: Base64URLString, |
|||
signature: Base64URLString, |
|||
) { |
|||
const authDataFromBase = Buffer.from(authenticatorData); |
|||
const clientDataFromBase = Buffer.from(clientDataJsonBase64Url, "base64"); |
|||
const sigBuffer = Buffer.from(signature, "base64"); |
|||
const finalSigBuffer = unwrapEC2Signature(sigBuffer); |
|||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
|||
|
|||
// Hash the client data
|
|||
const hash = sha256(clientDataFromBase); |
|||
|
|||
// Construct the preimage
|
|||
const preimage = Buffer.concat([authDataFromBase, hash]); |
|||
|
|||
const isValid = p256.verify( |
|||
finalSigBuffer, |
|||
new Uint8Array(preimage), |
|||
publicKeyBytes, |
|||
); |
|||
return isValid; |
|||
} |
|||
|
|||
export async function verifyJwtSimplewebauthn( |
|||
credIdHex: string, |
|||
issuerDid: string, |
|||
authenticatorData: ArrayBuffer, |
|||
challenge: Uint8Array, |
|||
clientDataJsonBase64Url: Base64URLString, |
|||
signature: Base64URLString, |
|||
) { |
|||
const authData = arrayToBase64Url(Buffer.from(authenticatorData)); |
|||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
|||
const credId = arrayBufferToBase64URLString( |
|||
Buffer.from(credIdHex, "hex").buffer, |
|||
); |
|||
const authOpts: VerifyAuthenticationResponseOpts = { |
|||
authenticator: { |
|||
credentialID: credId, |
|||
credentialPublicKey: publicKeyBytes, |
|||
counter: 0, |
|||
}, |
|||
expectedChallenge: arrayToBase64Url(challenge), |
|||
expectedOrigin: window.location.origin, |
|||
expectedRPID: window.location.hostname, |
|||
response: { |
|||
authenticatorAttachment: "platform", |
|||
clientExtensionResults: {}, |
|||
id: credId, |
|||
rawId: credId, |
|||
response: { |
|||
authenticatorData: authData, |
|||
clientDataJSON: clientDataJsonBase64Url, |
|||
signature: signature, |
|||
}, |
|||
type: "public-key", |
|||
}, |
|||
}; |
|||
const verification = await verifyAuthenticationResponse(authOpts); |
|||
return verification.verified; |
|||
} |
|||
|
|||
export async function verifyJwtWebCrypto( |
|||
credId: Base64URLString, |
|||
issuerDid: string, |
|||
authenticatorData: ArrayBuffer, |
|||
challenge: Uint8Array, |
|||
clientDataJsonBase64Url: Base64URLString, |
|||
signature: Base64URLString, |
|||
) { |
|||
const authDataFromBase = Buffer.from(authenticatorData); |
|||
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]); |
|||
|
|||
const publicKeyBytes = peerDidToPublicKeyBytes(issuerDid); |
|||
|
|||
const WebCrypto = await getWebCrypto(); |
|||
const verifyAlgorithm = { |
|||
name: "ECDSA", |
|||
hash: { name: "SHA-256" }, |
|||
}; |
|||
const publicKeyJwk = cborToKeys(publicKeyBytes).publicKeyJwk; |
|||
const keyAlgorithm = { |
|||
name: "ECDSA", |
|||
namedCurve: publicKeyJwk.crv, |
|||
}; |
|||
const publicKeyCryptoKey = await WebCrypto.subtle.importKey( |
|||
"jwk", |
|||
publicKeyJwk, |
|||
keyAlgorithm, |
|||
false, |
|||
["verify"], |
|||
); |
|||
const verified = await WebCrypto.subtle.verify( |
|||
verifyAlgorithm, |
|||
publicKeyCryptoKey, |
|||
finalSigBuffer, |
|||
preimage, |
|||
); |
|||
return verified; |
|||
} |
|||
|
|||
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 is basically hard-coded from https://www.w3.org/TR/did-core/#example-various-verification-method-types
|
|||
// (another reference is the @aviarytech/did-peer resolver)
|
|||
const id = did.split(":")[2]; |
|||
const multibase = id.slice(1); |
|||
const encnumbasis = multibase.slice(1); |
|||
const didDocument = { |
|||
"@context": [ |
|||
"https://www.w3.org/ns/did/v1", |
|||
"https://w3id.org/security/suites/jws-2020/v1", |
|||
], |
|||
assertionMethod: [did + "#" + encnumbasis], |
|||
authentication: [did + "#" + encnumbasis], |
|||
capabilityDelegation: [did + "#" + encnumbasis], |
|||
capabilityInvocation: [did + "#" + encnumbasis], |
|||
id: did, |
|||
keyAgreement: undefined, |
|||
service: undefined, |
|||
verificationMethod: [ |
|||
{ |
|||
controller: did, |
|||
id: did + "#" + encnumbasis, |
|||
publicKeyMultibase: multibase, |
|||
type: "EcdsaSecp256k1VerificationKey2019", |
|||
}, |
|||
], |
|||
}; |
|||
return { |
|||
didDocument, |
|||
didDocumentMetadata: {}, |
|||
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(/=+$/, ""); |
|||
} |
|||
|
|||
// from @simplewebauthn/browser
|
|||
function arrayBufferToBase64URLString(buffer) { |
|||
const bytes = new Uint8Array(buffer); |
|||
let str = ""; |
|||
for (const charCode of bytes) { |
|||
str += String.fromCharCode(charCode); |
|||
} |
|||
const base64String = btoa(str); |
|||
return base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); |
|||
} |
|||
|
|||
// from @simplewebauthn/browser
|
|||
function base64URLStringToArrayBuffer(base64URLString: string) { |
|||
const base64 = base64URLString.replace(/-/g, "+").replace(/_/g, "/"); |
|||
const padLength = (4 - (base64.length % 4)) % 4; |
|||
const padded = base64.padEnd(base64.length + padLength, "="); |
|||
const binary = atob(padded); |
|||
const buffer = new ArrayBuffer(binary.length); |
|||
const bytes = new Uint8Array(buffer); |
|||
for (let i = 0; i < binary.length; i++) { |
|||
bytes[i] = binary.charCodeAt(i); |
|||
} |
|||
return buffer; |
|||
} |
|||
|
|||
function cborToKeys(publicKeyBytes: Uint8Array) { |
|||
const jwkObj = cborDecode(publicKeyBytes); |
|||
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"], |
|||
); |
|||
} |
Loading…
Reference in new issue