| <html> |
| <head> |
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> |
| <title>webauthn test</title> |
| </head> |
| <body onload="init()"> |
| <h1>webauthn test</h1> |
| <p> |
| This is a demo/test page for generating FIDO keys and signatures in SSH |
| formats. The page initially displays a form to generate a FIDO key and |
| convert it to a SSH public key. |
| </p> |
| <p> |
| Once a key has been generated, an additional form will be displayed to |
| allow signing of data using the just-generated key. The data may be signed |
| as either a raw SSH signature or wrapped in a sshsig message (the latter is |
| easier to test using command-line tools. |
| </p> |
| <p> |
| Lots of debugging is printed along the way. |
| </p> |
| <h2>Enroll</h2> |
| <span id="error" style="color: #800; font-weight: bold; font-size: 150%;"></span> |
| <form id="enrollform"> |
| <table> |
| <tr> |
| <td><b>Username:</b></td> |
| <td><input id="username" type="text" size="20" name="user" value="test" /></td> |
| </tr> |
| <tr><td></td><td><input id="assertsubmit" type="submit" value="submit" /></td></tr> |
| </table> |
| </form> |
| <span id="enrollresult" style="visibility: hidden;"> |
| <h2>clientData</h2> |
| <pre id="enrollresultjson" style="color: #008; font-family: monospace;"></pre> |
| <h2>attestationObject raw</h2> |
| <pre id="enrollresultraw" style="color: #008; font-family: monospace;"></pre> |
| <h2>attestationObject</h2> |
| <pre id="enrollresultattestobj" style="color: #008; font-family: monospace;"></pre> |
| <h2>key handle</h2> |
| <pre id="keyhandle" style="color: #008; font-family: monospace;"></pre> |
| <h2>authData raw</h2> |
| <pre id="enrollresultauthdataraw" style="color: #008; font-family: monospace;"></pre> |
| <h2>authData</h2> |
| <pre id="enrollresultauthdata" style="color: #008; font-family: monospace;"></pre> |
| <h2>SSH pubkey blob</h2> |
| <pre id="enrollresultpkblob" style="color: #008; font-family: monospace;"></pre> |
| <h2>SSH pubkey string</h2> |
| <pre id="enrollresultpk" style="color: #008; font-family: monospace;"></pre> |
| <h2>SSH private key string</h2> |
| <pre id="enrollresultprivkey" style="color: #008; font-family: monospace;"></pre> |
| </span> |
| <span id="assertsection" style="visibility: hidden;"> |
| <h2>Assert</h2> |
| <form id="assertform"> |
| <span id="asserterror" style="color: #800; font-weight: bold;"></span> |
| <table> |
| <tr> |
| <td><b>Data to sign:</b></td> |
| <td><input id="message" type="text" size="20" name="message" value="test" /></td> |
| </tr> |
| <tr> |
| <td><input id="message_sshsig" type="checkbox" checked /> use sshsig format</td> |
| </tr> |
| <tr> |
| <td><b>Signature namespace:</b></td> |
| <td><input id="message_namespace" type="text" size="20" name="namespace" value="test" /></td> |
| </tr> |
| <tr><td></td><td><input type="submit" value="submit" /></td></tr> |
| </table> |
| </form> |
| </span> |
| <span id="assertresult" style="visibility: hidden;"> |
| <h2>clientData</h2> |
| <pre id="assertresultjson" style="color: #008; font-family: monospace;"></pre> |
| <h2>signature raw</h2> |
| <pre id="assertresultsigraw" style="color: #008; font-family: monospace;"></pre> |
| <h2>authenticatorData raw</h2> |
| <pre id="assertresultauthdataraw" style="color: #008; font-family: monospace;"></pre> |
| <h2>authenticatorData</h2> |
| <pre id="assertresultauthdata" style="color: #008; font-family: monospace;"></pre> |
| <h2>signature in SSH format</h2> |
| <pre id="assertresultsshsigraw" style="color: #008; font-family: monospace;"></pre> |
| <h2>signature in SSH format (base64 encoded)</h2> |
| <pre id="assertresultsshsigb64" style="color: #008; font-family: monospace;"></pre> |
| </span> |
| </body> |
| <script> |
| // ------------------------------------------------------------------ |
| // a crappy CBOR decoder - 20200401 djm@openbsd.org |
| |
| var CBORDecode = function(buffer) { |
| this.buf = buffer |
| this.v = new DataView(buffer) |
| this.offset = 0 |
| } |
| |
| CBORDecode.prototype.empty = function() { |
| return this.offset >= this.buf.byteLength |
| } |
| |
| CBORDecode.prototype.getU8 = function() { |
| let r = this.v.getUint8(this.offset) |
| this.offset += 1 |
| return r |
| } |
| |
| CBORDecode.prototype.getU16 = function() { |
| let r = this.v.getUint16(this.offset) |
| this.offset += 2 |
| return r |
| } |
| |
| CBORDecode.prototype.getU32 = function() { |
| let r = this.v.getUint32(this.offset) |
| this.offset += 4 |
| return r |
| } |
| |
| CBORDecode.prototype.getU64 = function() { |
| let r = this.v.getUint64(this.offset) |
| this.offset += 8 |
| return r |
| } |
| |
| CBORDecode.prototype.getCBORTypeLen = function() { |
| let tl, t, l |
| tl = this.getU8() |
| t = (tl & 0xe0) >> 5 |
| l = tl & 0x1f |
| return [t, this.decodeInteger(l)] |
| } |
| |
| CBORDecode.prototype.decodeInteger = function(len) { |
| switch (len) { |
| case 0x18: return this.getU8() |
| case 0x19: return this.getU16() |
| case 0x20: return this.getU32() |
| case 0x21: return this.getU64() |
| default: |
| if (len <= 23) { |
| return len |
| } |
| throw new Error("Unsupported int type 0x" + len.toString(16)) |
| } |
| } |
| |
| CBORDecode.prototype.decodeNegint = function(len) { |
| let r = -(this.decodeInteger(len) + 1) |
| return r |
| } |
| |
| CBORDecode.prototype.decodeByteString = function(len) { |
| let r = this.buf.slice(this.offset, this.offset + len) |
| this.offset += len |
| return r |
| } |
| |
| CBORDecode.prototype.decodeTextString = function(len) { |
| let u8dec = new TextDecoder('utf-8') |
| r = u8dec.decode(this.decodeByteString(len)) |
| return r |
| } |
| |
| CBORDecode.prototype.decodeArray = function(len, level) { |
| let r = [] |
| for (let i = 0; i < len; i++) { |
| let v = this.decodeInternal(level) |
| r.push(v) |
| // console.log("decodeArray level " + level.toString() + " index " + i.toString() + " value " + JSON.stringify(v)) |
| } |
| return r |
| } |
| |
| CBORDecode.prototype.decodeMap = function(len, level) { |
| let r = {} |
| for (let i = 0; i < len; i++) { |
| let k = this.decodeInternal(level) |
| let v = this.decodeInternal(level) |
| r[k] = v |
| // console.log("decodeMap level " + level.toString() + " key " + k.toString() + " value " + JSON.stringify(v)) |
| // XXX check string keys, duplicates |
| } |
| return r |
| } |
| |
| CBORDecode.prototype.decodePrimitive = function(t) { |
| switch (t) { |
| case 20: return false |
| case 21: return true |
| case 22: return null |
| case 23: return undefined |
| default: |
| throw new Error("Unsupported primitive 0x" + t.toString(2)) |
| } |
| } |
| |
| CBORDecode.prototype.decodeInternal = function(level) { |
| if (level > 256) { |
| throw new Error("CBOR nesting too deep") |
| } |
| let t, l, r |
| [t, l] = this.getCBORTypeLen() |
| // console.log("decode level " + level.toString() + " type " + t.toString() + " len " + l.toString()) |
| switch (t) { |
| case 0: |
| r = this.decodeInteger(l) |
| break |
| case 1: |
| r = this.decodeNegint(l) |
| break |
| case 2: |
| r = this.decodeByteString(l) |
| break |
| case 3: |
| r = this.decodeTextString(l) |
| break |
| case 4: |
| r = this.decodeArray(l, level + 1) |
| break |
| case 5: |
| r = this.decodeMap(l, level + 1) |
| break |
| case 6: |
| console.log("XXX ignored semantic tag " + this.decodeInteger(l).toString()) |
| break; |
| case 7: |
| r = this.decodePrimitive(l) |
| break |
| default: |
| throw new Error("Unsupported type 0x" + t.toString(2) + " len " + l.toString()) |
| } |
| // console.log("decode level " + level.toString() + " value " + JSON.stringify(r)) |
| return r |
| } |
| |
| CBORDecode.prototype.decode = function() { |
| return this.decodeInternal(0) |
| } |
| |
| // ------------------------------------------------------------------ |
| // a crappy SSH message packer - 20200401 djm@openbsd.org |
| |
| var SSHMSG = function() { |
| this.r = [] |
| } |
| |
| SSHMSG.prototype.length = function() { |
| let len = 0 |
| for (buf of this.r) { |
| len += buf.length |
| } |
| return len |
| } |
| |
| SSHMSG.prototype.serialise = function() { |
| let r = new ArrayBuffer(this.length()) |
| let v = new Uint8Array(r) |
| let offset = 0 |
| for (buf of this.r) { |
| v.set(buf, offset) |
| offset += buf.length |
| } |
| if (offset != r.byteLength) { |
| throw new Error("djm can't count") |
| } |
| return r |
| } |
| |
| SSHMSG.prototype.serialiseBase64 = function(v) { |
| let b = this.serialise() |
| return btoa(String.fromCharCode(...new Uint8Array(b))); |
| } |
| |
| SSHMSG.prototype.putU8 = function(v) { |
| this.r.push(new Uint8Array([v])) |
| } |
| |
| SSHMSG.prototype.putU32 = function(v) { |
| this.r.push(new Uint8Array([ |
| (v >> 24) & 0xff, |
| (v >> 16) & 0xff, |
| (v >> 8) & 0xff, |
| (v & 0xff) |
| ])) |
| } |
| |
| SSHMSG.prototype.put = function(v) { |
| this.r.push(new Uint8Array(v)) |
| } |
| |
| SSHMSG.prototype.putStringRaw = function(v) { |
| let enc = new TextEncoder(); |
| let venc = enc.encode(v) |
| this.put(venc) |
| } |
| |
| SSHMSG.prototype.putString = function(v) { |
| let enc = new TextEncoder(); |
| let venc = enc.encode(v) |
| this.putU32(venc.length) |
| this.put(venc) |
| } |
| |
| SSHMSG.prototype.putSSHMSG = function(v) { |
| let msg = v.serialise() |
| this.putU32(msg.byteLength) |
| this.put(msg) |
| } |
| |
| SSHMSG.prototype.putBytes = function(v) { |
| this.putU32(v.byteLength) |
| this.put(v) |
| } |
| |
| SSHMSG.prototype.putECPoint = function(x, y) { |
| let x8 = new Uint8Array(x) |
| let y8 = new Uint8Array(y) |
| this.putU32(1 + x8.length + y8.length) |
| this.putU8(0x04) // Uncompressed point format. |
| this.put(x8) |
| this.put(y8) |
| } |
| |
| // ------------------------------------------------------------------ |
| // webauthn to SSH glue - djm@openbsd.org 20200408 |
| |
| function error(msg, ...args) { |
| document.getElementById("error").innerText = msg |
| console.log(msg) |
| for (const arg of args) { |
| console.dir(arg) |
| } |
| } |
| function hexdump(buf) { |
| const hex = Array.from(new Uint8Array(buf)).map( |
| b => b.toString(16).padStart(2, "0")) |
| const fmt = new Array() |
| for (let i = 0; i < hex.length; i++) { |
| if ((i % 16) == 0) { |
| // Prepend length every 16 bytes. |
| fmt.push(i.toString(16).padStart(4, "0")) |
| fmt.push(" ") |
| } |
| fmt.push(hex[i]) |
| fmt.push(" ") |
| if ((i % 16) == 15) { |
| fmt.push("\n") |
| } |
| } |
| return fmt.join("") |
| } |
| function enrollform_submit(event) { |
| event.preventDefault(); |
| console.log("submitted") |
| username = event.target.elements.username.value |
| if (username === "") { |
| error("no username specified") |
| return false |
| } |
| enrollStart(username) |
| } |
| function enrollStart(username) { |
| let challenge = new Uint8Array(32) |
| window.crypto.getRandomValues(challenge) |
| let userid = new Uint8Array(8) |
| window.crypto.getRandomValues(userid) |
| |
| console.log("challenge:" + btoa(challenge)) |
| console.log("userid:" + btoa(userid)) |
| |
| let pkopts = { |
| challenge: challenge, |
| rp: { |
| name: "mindrot.org", |
| id: "mindrot.org", |
| }, |
| user: { |
| id: userid, |
| name: username, |
| displayName: username, |
| }, |
| authenticatorSelection: { |
| authenticatorAttachment: "cross-platform", |
| userVerification: "discouraged", |
| }, |
| pubKeyCredParams: [{alg: -7, type: "public-key"}], // ES256 |
| timeout: 30 * 1000, |
| }; |
| console.dir(pkopts) |
| window.enrollOpts = pkopts |
| let credpromise = navigator.credentials.create({ publicKey: pkopts }); |
| credpromise.then(enrollSuccess, enrollFailure) |
| } |
| function enrollFailure(result) { |
| error("Enroll failed", result) |
| } |
| function enrollSuccess(result) { |
| console.log("Enroll succeeded") |
| console.dir(result) |
| window.enrollResult = result |
| document.getElementById("enrollresult").style.visibility = "visible" |
| |
| // Show the clientData |
| let u8dec = new TextDecoder('utf-8') |
| clientData = u8dec.decode(result.response.clientDataJSON) |
| document.getElementById("enrollresultjson").innerText = clientData |
| |
| // Show the raw key handle. |
| document.getElementById("keyhandle").innerText = hexdump(result.rawId) |
| |
| // Decode and show the attestationObject |
| document.getElementById("enrollresultraw").innerText = hexdump(result.response.attestationObject) |
| let aod = new CBORDecode(result.response.attestationObject) |
| let attestationObject = aod.decode() |
| console.log("attestationObject") |
| console.dir(attestationObject) |
| document.getElementById("enrollresultattestobj").innerText = JSON.stringify(attestationObject) |
| |
| // Decode and show the authData |
| document.getElementById("enrollresultauthdataraw").innerText = hexdump(attestationObject.authData) |
| let authData = decodeAuthenticatorData(attestationObject.authData, true) |
| console.log("authData") |
| console.dir(authData) |
| window.enrollAuthData = authData |
| document.getElementById("enrollresultauthdata").innerText = JSON.stringify(authData) |
| |
| // Reformat the pubkey as a SSH key for easy verification |
| window.rawKey = reformatPubkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id) |
| console.log("SSH pubkey blob") |
| console.dir(window.rawKey) |
| document.getElementById("enrollresultpkblob").innerText = hexdump(window.rawKey) |
| let pk64 = btoa(String.fromCharCode(...new Uint8Array(window.rawKey))); |
| let pk = "sk-ecdsa-sha2-nistp256@openssh.com " + pk64 |
| document.getElementById("enrollresultpk").innerText = pk |
| |
| // Format a private key too. |
| flags = 0x01 // SSH_SK_USER_PRESENCE_REQD |
| window.rawPrivkey = reformatPrivkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id, result.rawId, flags) |
| let privkeyFileBlob = privkeyFile(window.rawKey, window.rawPrivkey, window.enrollOpts.user.name, window.enrollOpts.rp.id) |
| let privk64 = btoa(String.fromCharCode(...new Uint8Array(privkeyFileBlob))); |
| let privkey = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + wrapString(privk64, 70) + "-----END OPENSSH PRIVATE KEY-----\n" |
| document.getElementById("enrollresultprivkey").innerText = privkey |
| |
| // Success: show the assertion form. |
| document.getElementById("assertsection").style.visibility = "visible" |
| } |
| |
| function decodeAuthenticatorData(authData, expectCred) { |
| let r = new Object() |
| let v = new DataView(authData) |
| |
| r.rpIdHash = authData.slice(0, 32) |
| r.flags = v.getUint8(32) |
| r.signCount = v.getUint32(33) |
| |
| // Decode attestedCredentialData if present. |
| let offset = 37 |
| let acd = new Object() |
| if (expectCred) { |
| acd.aaguid = authData.slice(offset, offset+16) |
| offset += 16 |
| let credentialIdLength = v.getUint16(offset) |
| offset += 2 |
| acd.credentialIdLength = credentialIdLength |
| acd.credentialId = authData.slice(offset, offset+credentialIdLength) |
| offset += credentialIdLength |
| r.attestedCredentialData = acd |
| } |
| console.log("XXXXX " + offset.toString()) |
| let pubkeyrest = authData.slice(offset, authData.byteLength) |
| let pkdecode = new CBORDecode(pubkeyrest) |
| if (expectCred) { |
| // XXX unsafe: doesn't mandate COSE canonical format. |
| acd.credentialPublicKey = pkdecode.decode() |
| } |
| if (!pkdecode.empty()) { |
| // Decode extensions if present. |
| r.extensions = pkdecode.decode() |
| } |
| return r |
| } |
| |
| function wrapString(s, l) { |
| ret = "" |
| for (i = 0; i < s.length; i += l) { |
| ret += s.slice(i, i + l) + "\n" |
| } |
| return ret |
| } |
| |
| function checkPubkey(pk) { |
| // pk is in COSE format. We only care about a tiny subset. |
| if (pk[1] != 2) { |
| console.dir(pk) |
| throw new Error("pubkey is not EC") |
| } |
| if (pk[-1] != 1) { |
| throw new Error("pubkey is not in P256") |
| } |
| if (pk[3] != -7) { |
| throw new Error("pubkey is not ES256") |
| } |
| if (pk[-2].byteLength != 32 || pk[-3].byteLength != 32) { |
| throw new Error("pubkey EC coords have bad length") |
| } |
| } |
| |
| function reformatPubkey(pk, rpid) { |
| checkPubkey(pk) |
| let msg = new SSHMSG() |
| msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type |
| msg.putString("nistp256") // Key curve |
| msg.putECPoint(pk[-2], pk[-3]) // EC key |
| msg.putString(rpid) // RP ID |
| return msg.serialise() |
| } |
| |
| function reformatPrivkey(pk, rpid, kh, flags) { |
| checkPubkey(pk) |
| let msg = new SSHMSG() |
| msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type |
| msg.putString("nistp256") // Key curve |
| msg.putECPoint(pk[-2], pk[-3]) // EC key |
| msg.putString(rpid) // RP ID |
| msg.putU8(flags) // flags |
| msg.putBytes(kh) // handle |
| msg.putString("") // reserved |
| return msg.serialise() |
| } |
| |
| function privkeyFile(pub, priv, user, rp) { |
| let innerMsg = new SSHMSG() |
| innerMsg.putU32(0xdeadbeef) // check byte |
| innerMsg.putU32(0xdeadbeef) // check byte |
| innerMsg.put(priv) // privkey |
| innerMsg.putString("webauthn.html " + user + "@" + rp) // comment |
| // Pad to cipher blocksize (8). |
| p = 1 |
| while (innerMsg.length() % 8 != 0) { |
| innerMsg.putU8(p++) |
| } |
| let msg = new SSHMSG() |
| msg.putStringRaw("openssh-key-v1") // Magic |
| msg.putU8(0) // \0 terminate |
| msg.putString("none") // cipher |
| msg.putString("none") // KDF |
| msg.putString("") // KDF options |
| msg.putU32(1) // nkeys |
| msg.putBytes(pub) // pubkey |
| msg.putSSHMSG(innerMsg) // inner |
| //msg.put(innerMsg.serialise()) // inner |
| return msg.serialise() |
| } |
| |
| async function assertform_submit(event) { |
| event.preventDefault(); |
| console.log("submitted") |
| message = event.target.elements.message.value |
| if (message === "") { |
| error("no message specified") |
| return false |
| } |
| let enc = new TextEncoder() |
| let encmsg = enc.encode(message) |
| window.assertSignRaw = !event.target.elements.message_sshsig.checked |
| console.log("using sshsig ", !window.assertSignRaw) |
| if (window.assertSignRaw) { |
| assertStart(encmsg) |
| return |
| } |
| // Format a sshsig-style message. |
| window.sigHashAlg = "sha512" |
| let msghash = await crypto.subtle.digest("SHA-512", encmsg); |
| console.log("raw message hash") |
| console.dir(msghash) |
| window.sigNamespace = event.target.elements.message_namespace.value |
| let sigbuf = new SSHMSG() |
| sigbuf.put(enc.encode("SSHSIG")) |
| sigbuf.putString(window.sigNamespace) |
| sigbuf.putU32(0) // Reserved string |
| sigbuf.putString(window.sigHashAlg) |
| sigbuf.putBytes(msghash) |
| let msg = sigbuf.serialise() |
| console.log("sigbuf") |
| console.dir(msg) |
| assertStart(msg) |
| } |
| |
| function assertStart(message) { |
| let assertReqOpts = { |
| challenge: message, |
| rpId: "mindrot.org", |
| allowCredentials: [{ |
| type: 'public-key', |
| id: window.enrollResult.rawId, |
| }], |
| userVerification: "discouraged", |
| timeout: (30 * 1000), |
| } |
| console.log("assertReqOpts") |
| console.dir(assertReqOpts) |
| window.assertReqOpts = assertReqOpts |
| let assertpromise = navigator.credentials.get({ |
| publicKey: assertReqOpts |
| }); |
| assertpromise.then(assertSuccess, assertFailure) |
| } |
| function assertFailure(result) { |
| error("Assertion failed", result) |
| } |
| function linewrap(s) { |
| const linelen = 70 |
| let ret = "" |
| for (let i = 0; i < s.length; i += linelen) { |
| end = i + linelen |
| if (end > s.length) { |
| end = s.length |
| } |
| if (i > 0) { |
| ret += "\n" |
| } |
| ret += s.slice(i, end) |
| } |
| return ret + "\n" |
| } |
| function assertSuccess(result) { |
| console.log("Assertion succeeded") |
| console.dir(result) |
| window.assertResult = result |
| document.getElementById("assertresult").style.visibility = "visible" |
| |
| // show the clientData. |
| let u8dec = new TextDecoder('utf-8') |
| clientData = u8dec.decode(result.response.clientDataJSON) |
| document.getElementById("assertresultjson").innerText = clientData |
| |
| // show the signature. |
| document.getElementById("assertresultsigraw").innerText = hexdump(result.response.signature) |
| |
| // decode and show the authData. |
| document.getElementById("assertresultauthdataraw").innerText = hexdump(result.response.authenticatorData) |
| authData = decodeAuthenticatorData(result.response.authenticatorData, false) |
| document.getElementById("assertresultauthdata").innerText = JSON.stringify(authData) |
| |
| // Parse and reformat the signature to an SSH style signature. |
| let sshsig = reformatSignature(result.response.signature, clientData, authData) |
| document.getElementById("assertresultsshsigraw").innerText = hexdump(sshsig) |
| let sig64 = btoa(String.fromCharCode(...new Uint8Array(sshsig))); |
| if (window.assertSignRaw) { |
| document.getElementById("assertresultsshsigb64").innerText = sig64 |
| } else { |
| document.getElementById("assertresultsshsigb64").innerText = |
| "-----BEGIN SSH SIGNATURE-----\n" + linewrap(sig64) + |
| "-----END SSH SIGNATURE-----\n"; |
| } |
| } |
| |
| function reformatSignature(sig, clientData, authData) { |
| if (sig.byteLength < 2) { |
| throw new Error("signature is too short") |
| } |
| let offset = 0 |
| let v = new DataView(sig) |
| // Expect an ASN.1 SEQUENCE that exactly spans the signature. |
| if (v.getUint8(offset) != 0x30) { |
| throw new Error("signature not an ASN.1 sequence") |
| } |
| offset++ |
| let seqlen = v.getUint8(offset) |
| offset++ |
| if ((seqlen & 0x80) != 0 || seqlen != sig.byteLength - offset) { |
| throw new Error("signature has unexpected length " + seqlen.toString() + " vs expected " + (sig.byteLength - offset).toString()) |
| } |
| |
| // Parse 'r' INTEGER value. |
| if (v.getUint8(offset) != 0x02) { |
| throw new Error("signature r not an ASN.1 integer") |
| } |
| offset++ |
| let rlen = v.getUint8(offset) |
| offset++ |
| if ((rlen & 0x80) != 0 || rlen > sig.byteLength - offset) { |
| throw new Error("signature r has unexpected length " + rlen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) |
| } |
| let r = sig.slice(offset, offset + rlen) |
| offset += rlen |
| console.log("sig_r") |
| console.dir(r) |
| |
| // Parse 's' INTEGER value. |
| if (v.getUint8(offset) != 0x02) { |
| throw new Error("signature r not an ASN.1 integer") |
| } |
| offset++ |
| let slen = v.getUint8(offset) |
| offset++ |
| if ((slen & 0x80) != 0 || slen > sig.byteLength - offset) { |
| throw new Error("signature s has unexpected length " + slen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) |
| } |
| let s = sig.slice(offset, offset + slen) |
| console.log("sig_s") |
| console.dir(s) |
| offset += slen |
| |
| if (offset != sig.byteLength) { |
| throw new Error("unexpected final offset during signature parsing " + offset.toString() + " expected " + sig.byteLength.toString()) |
| } |
| |
| // Reformat as an SSH signature. |
| let clientDataParsed = JSON.parse(clientData) |
| let innersig = new SSHMSG() |
| innersig.putBytes(r) |
| innersig.putBytes(s) |
| |
| let rawsshsig = new SSHMSG() |
| rawsshsig.putString("webauthn-sk-ecdsa-sha2-nistp256@openssh.com") |
| rawsshsig.putSSHMSG(innersig) |
| rawsshsig.putU8(authData.flags) |
| rawsshsig.putU32(authData.signCount) |
| rawsshsig.putString(clientDataParsed.origin) |
| rawsshsig.putString(clientData) |
| if (authData.extensions == undefined) { |
| rawsshsig.putU32(0) |
| } else { |
| rawsshsig.putBytes(authData.extensions) |
| } |
| |
| if (window.assertSignRaw) { |
| return rawsshsig.serialise() |
| } |
| // Format as SSHSIG. |
| let enc = new TextEncoder() |
| let sshsig = new SSHMSG() |
| sshsig.put(enc.encode("SSHSIG")) |
| sshsig.putU32(0x01) // Signature version. |
| sshsig.putBytes(window.rawKey) |
| sshsig.putString(window.sigNamespace) |
| sshsig.putU32(0) // Reserved string |
| sshsig.putString(window.sigHashAlg) |
| sshsig.putBytes(rawsshsig.serialise()) |
| return sshsig.serialise() |
| } |
| |
| function toggleNamespaceVisibility() { |
| const assertsigtype = document.getElementById('message_sshsig'); |
| const assertsignamespace = document.getElementById('message_namespace'); |
| assertsignamespace.disabled = !assertsigtype.checked; |
| } |
| |
| function init() { |
| if (document.location.protocol != "https:") { |
| error("This page must be loaded via https") |
| const assertsubmit = document.getElementById('assertsubmit') |
| assertsubmit.disabled = true |
| } |
| const enrollform = document.getElementById('enrollform'); |
| enrollform.addEventListener('submit', enrollform_submit); |
| const assertform = document.getElementById('assertform'); |
| assertform.addEventListener('submit', assertform_submit); |
| const assertsigtype = document.getElementById('message_sshsig'); |
| assertsigtype.onclick = toggleNamespaceVisibility; |
| } |
| </script> |
| |
| </html> |