Skip to content

Instantly share code, notes, and snippets.

@sisou
Forked from philholden/webauthn.js
Last active September 18, 2023 14:32
Show Gist options
  • Save sisou/a364836483a923921bf0f57c64db8c8c to your computer and use it in GitHub Desktop.
Save sisou/a364836483a923921bf0f57c64db8c8c to your computer and use it in GitHub Desktop.
A demo for signing and verifying Nimiq transaction signatures with WebAuthn
node_modules
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nimiq Webauthn Signing Demo</title>
</head>
<body>
<script src="./index.js" type="module"></script>
</body>
</html>
import init, * as Nimiq from './node_modules/@nimiq/core-web/web/index.js';
import { decode as fromBase64 } from './node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js';
import { register } from './register.js';
import { sign } from './sign.js';
await init();
const rawCredentialInfo = localStorage.getItem("credentialInfo");
/**
* @type {{ id: string, publicKey: string} | undefined }
*/
let credentialInfo;
if (rawCredentialInfo) {
credentialInfo = JSON.parse(rawCredentialInfo);
} else {
credentialInfo = await register();
localStorage.setItem("credentialInfo", JSON.stringify(credentialInfo));
}
const publicKeyHash = Nimiq.Hash.computeBlake2b(new Uint8Array(fromBase64(credentialInfo.publicKey)));
const sender = new Nimiq.Address(publicKeyHash);
const tx = new Nimiq.Transaction(
sender, Nimiq.AccountType.Basic, new Uint8Array(0),
new Nimiq.Address(new Uint8Array(20)), Nimiq.AccountType.Basic, new Uint8Array(0),
100_00000n, // value
0n, // fee
0, // flags
1, // validityStartHeight
5, // networkId
);
console.log("MESSAGE", tx.serializeContent());
const proof = await sign(
tx,
fromBase64(credentialInfo.id),
new Uint8Array(fromBase64(credentialInfo.publicKey)),
);
tx.proof = proof;
console.log(tx);
/** @type {boolean} */
let verified;
try {
tx.verify();
verified = true;
} catch (e) {
console.error(e);
verified = false;
}
console.log("verified", verified);
if (!verified) {
throw new Error('Verification failed');
}
console.log(tx.toPlain())
console.log(tx.toHex())
{
"name": "nimiq-webauthn-signing",
"version": "0.0.0",
"main": "index.js",
"repository": "[email protected]:a364836483a923921bf0f57c64db8c8c.git",
"author": "Sören <[email protected]>",
"license": "MIT",
"dependencies": {
"@nimiq/core-web": "../../nimiq/core-rs-albatross/web-client/dist",
"@smithy/util-hex-encoding": "^2.0.0",
"base64-arraybuffer": "^1.0.2"
}
}
import { encode as toBase64 } from './node_modules/base64-arraybuffer/dist/base64-arraybuffer.es5.js';
export async function register() {
const registrationChallenge = crypto.getRandomValues(new Uint8Array(32));
/**
* @type {CredentialCreationOptions}
*/
var createCredentialOptions = {
publicKey: {
// Relying Party (a.k.a. - Service):
rp: {
name: "Nimiq Keyguard",
},
// User:
user: {
id: new Uint8Array(16), // Empty array
name: "Nimiq Webauthn Account",
displayName: "Nimiq Account",
},
pubKeyCredParams: [{
type: "public-key",
alg: -7, // ES256 = ECDSA over P-256 with SHA-256
}],
authenticatorSelection: {
userVerification: "preferred", // Should be "required", but that excludes Ledgers
},
timeout: 60e3, // 1 minute
challenge: registrationChallenge.buffer,
}
};
// register/create a new credential
/**
* @type {PublicKeyCredential | null}
*/
var cred = await navigator.credentials.create(createCredentialOptions);
if (!cred) throw new Error("No credential created");
console.log("New credential", cred);
// Convert public key from "spki" to "raw" format
let publicKey = /** @type {AuthenticatorAttestationResponse} */ (cred.response).getPublicKey();
if (!publicKey) throw new Error("No public key received");
publicKey = await compressKey(publicKey);
return {
id: toBase64(cred.rawId),
publicKey: toBase64(publicKey),
};
}
/**
* @param {ArrayBuffer} spkiUncompressed
*/
async function compressKey(spkiUncompressed) {
if (spkiUncompressed.byteLength !== 91) {
throw new Error('Invalid public key length, expected 91 bytes for "spki" format');
}
// Import as spki and reexport as raw
const cryptoKey = await crypto.subtle.importKey(
"spki",
spkiUncompressed,
{
// these are the algorithm options
// await cred.response.getPublicKeyAlgorithm() // returns -7
// -7 is ES256 with P-256 // search -7 in https://w3c.github.io/webauthn
// the W3C webcrypto docs:
//
// https://www.w3.org/TR/WebCryptoAPI/#informative-references (scroll down a bit)
//
// ES256 corrisponds with the following AlgorithmIdentifier:
name: "ECDSA",
namedCurve: "P-256",
hash: { name: "SHA-256" },
},
true, // extractable
["verify"], // "verify" for public key import
);
const rawKey = await crypto.subtle.exportKey("raw", cryptoKey);
console.log("rawKey", new Uint8Array(rawKey));
// Compress public key
if (rawKey.byteLength !== 65) {
throw new Error('Invalid raw key length, expected 65 bytes for "raw" format');
}
// Get the prefix and x coordinate
const compressed = new Uint8Array(rawKey.slice(0, 33));
// Adjust prefix according to y coordinate eveness
compressed[0] = 0x02 | (spkiUncompressed[spkiUncompressed.byteLength - 1] & 0x01);
return compressed.buffer;
}
import * as Nimiq from './node_modules/@nimiq/core-web/web/index.js';
import { fromHex } from './node_modules/@smithy/util-hex-encoding/dist-es/index.js';
/**
* @param {Nimiq.Transaction} tx
* @param {ArrayBuffer} credentialId
* @param {Uint8Array} publicKey
*/
export async function sign(tx, credentialId, publicKey) {
/**
* @type {CredentialRequestOptions}
*/
var credentialRequestOptions = {
publicKey: {
timeout: 60e3, // 1 minute
allowCredentials: [{
id: credentialId,
transports: ["usb", "nfc", "ble"],
type: "public-key",
userVerification: "required",
}],
challenge: fromHex(tx.hash()).buffer,
},
};
/**
* @type {PublicKeyCredential | null}
*/
var assertion = await navigator.credentials.get(credentialRequestOptions);
if (!assertion) throw new Error("No assertation received");
console.log("Assertion", assertion);
var asn1Signature = /** @type {AuthenticatorAssertionResponse} */ (assertion.response).signature;
var authenticatorData = new Uint8Array(/** @type {AuthenticatorAssertionResponse} */ (assertion.response).authenticatorData);
const proof = Nimiq.SignatureProof.webauthnSingleSig(
new Nimiq.WebauthnPublicKey(publicKey),
Nimiq.Signature.fromAsn1(new Uint8Array(asn1Signature)),
location.host,
authenticatorData,
);
return proof.serializeExtended();
}
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@nimiq/core-web@../../nimiq/core-rs-albatross/web-client/dist":
version "2.0.0-alpha.15"
dependencies:
comlink "^4.4.1"
websocket "^1.0.34"
"@smithy/util-hex-encoding@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz#0aa3515acd2b005c6d55675e377080a7c513b59e"
integrity sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==
dependencies:
tslib "^2.5.0"
base64-arraybuffer@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz#1c37589a7c4b0746e34bd1feb951da2df01c1bdc"
integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
bufferutil@^4.0.1:
version "4.0.7"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad"
integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==
dependencies:
node-gyp-build "^4.3.0"
comlink@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.1.tgz#e568b8e86410b809e8600eb2cf40c189371ef981"
integrity sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
debug@^2.2.0:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
es5-ext@^0.10.35, es5-ext@^0.10.50:
version "0.10.62"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
next-tick "^1.1.0"
es6-iterator@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.1, es6-symbol@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
ext@^1.1.2:
version "1.7.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
dependencies:
type "^2.7.2"
is-typedarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
node-gyp-build@^4.3.0:
version "4.6.1"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e"
integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ==
tslib@^2.5.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
type@^1.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
dependencies:
is-typedarray "^1.0.0"
utf-8-validate@^5.0.2:
version "5.0.10"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"
integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==
dependencies:
node-gyp-build "^4.3.0"
websocket@^1.0.34:
version "1.0.34"
resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111"
integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==
dependencies:
bufferutil "^4.0.1"
debug "^2.2.0"
es5-ext "^0.10.50"
typedarray-to-buffer "^3.1.5"
utf-8-validate "^5.0.2"
yaeti "^0.0.6"
yaeti@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment