119261079SEd Maste<html> 219261079SEd Maste<head> 319261079SEd Maste<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 419261079SEd Maste<title>webauthn test</title> 519261079SEd Maste</head> 619261079SEd Maste<body onload="init()"> 719261079SEd Maste<h1>webauthn test</h1> 819261079SEd Maste<p> 919261079SEd MasteThis is a demo/test page for generating FIDO keys and signatures in SSH 1019261079SEd Masteformats. The page initially displays a form to generate a FIDO key and 1119261079SEd Masteconvert it to a SSH public key. 1219261079SEd Maste</p> 1319261079SEd Maste<p> 1419261079SEd MasteOnce a key has been generated, an additional form will be displayed to 1519261079SEd Masteallow signing of data using the just-generated key. The data may be signed 1619261079SEd Masteas either a raw SSH signature or wrapped in a sshsig message (the latter is 1719261079SEd Masteeasier to test using command-line tools. 1819261079SEd Maste</p> 1919261079SEd Maste<p> 2019261079SEd MasteLots of debugging is printed along the way. 2119261079SEd Maste</p> 2219261079SEd Maste<h2>Enroll</h2> 2319261079SEd Maste<span id="error" style="color: #800; font-weight: bold; font-size: 150%;"></span> 2419261079SEd Maste<form id="enrollform"> 2519261079SEd Maste<table> 2619261079SEd Maste<tr> 2719261079SEd Maste<td><b>Username:</b></td> 2819261079SEd Maste<td><input id="username" type="text" size="20" name="user" value="test" /></td> 2919261079SEd Maste</tr> 3019261079SEd Maste<tr><td></td><td><input id="assertsubmit" type="submit" value="submit" /></td></tr> 3119261079SEd Maste</table> 3219261079SEd Maste</form> 3319261079SEd Maste<span id="enrollresult" style="visibility: hidden;"> 3419261079SEd Maste<h2>clientData</h2> 3519261079SEd Maste<pre id="enrollresultjson" style="color: #008; font-family: monospace;"></pre> 3619261079SEd Maste<h2>attestationObject raw</h2> 3719261079SEd Maste<pre id="enrollresultraw" style="color: #008; font-family: monospace;"></pre> 3819261079SEd Maste<h2>attestationObject</h2> 3919261079SEd Maste<pre id="enrollresultattestobj" style="color: #008; font-family: monospace;"></pre> 4019261079SEd Maste<h2>key handle</h2> 4119261079SEd Maste<pre id="keyhandle" style="color: #008; font-family: monospace;"></pre> 4219261079SEd Maste<h2>authData raw</h2> 4319261079SEd Maste<pre id="enrollresultauthdataraw" style="color: #008; font-family: monospace;"></pre> 4419261079SEd Maste<h2>authData</h2> 4519261079SEd Maste<pre id="enrollresultauthdata" style="color: #008; font-family: monospace;"></pre> 4619261079SEd Maste<h2>SSH pubkey blob</h2> 4719261079SEd Maste<pre id="enrollresultpkblob" style="color: #008; font-family: monospace;"></pre> 4819261079SEd Maste<h2>SSH pubkey string</h2> 4919261079SEd Maste<pre id="enrollresultpk" style="color: #008; font-family: monospace;"></pre> 5019261079SEd Maste<h2>SSH private key string</h2> 5119261079SEd Maste<pre id="enrollresultprivkey" style="color: #008; font-family: monospace;"></pre> 5219261079SEd Maste</span> 5319261079SEd Maste<span id="assertsection" style="visibility: hidden;"> 5419261079SEd Maste<h2>Assert</h2> 5519261079SEd Maste<form id="assertform"> 5619261079SEd Maste<span id="asserterror" style="color: #800; font-weight: bold;"></span> 5719261079SEd Maste<table> 5819261079SEd Maste<tr> 5919261079SEd Maste<td><b>Data to sign:</b></td> 6019261079SEd Maste<td><input id="message" type="text" size="20" name="message" value="test" /></td> 6119261079SEd Maste</tr> 6219261079SEd Maste<tr> 6319261079SEd Maste<td><input id="message_sshsig" type="checkbox" checked /> use sshsig format</td> 6419261079SEd Maste</tr> 6519261079SEd Maste<tr> 6619261079SEd Maste<td><b>Signature namespace:</b></td> 6719261079SEd Maste<td><input id="message_namespace" type="text" size="20" name="namespace" value="test" /></td> 6819261079SEd Maste</tr> 6919261079SEd Maste<tr><td></td><td><input type="submit" value="submit" /></td></tr> 7019261079SEd Maste</table> 7119261079SEd Maste</form> 7219261079SEd Maste</span> 7319261079SEd Maste<span id="assertresult" style="visibility: hidden;"> 7419261079SEd Maste<h2>clientData</h2> 7519261079SEd Maste<pre id="assertresultjson" style="color: #008; font-family: monospace;"></pre> 7619261079SEd Maste<h2>signature raw</h2> 7719261079SEd Maste<pre id="assertresultsigraw" style="color: #008; font-family: monospace;"></pre> 7819261079SEd Maste<h2>authenticatorData raw</h2> 7919261079SEd Maste<pre id="assertresultauthdataraw" style="color: #008; font-family: monospace;"></pre> 8019261079SEd Maste<h2>authenticatorData</h2> 8119261079SEd Maste<pre id="assertresultauthdata" style="color: #008; font-family: monospace;"></pre> 8219261079SEd Maste<h2>signature in SSH format</h2> 8319261079SEd Maste<pre id="assertresultsshsigraw" style="color: #008; font-family: monospace;"></pre> 8419261079SEd Maste<h2>signature in SSH format (base64 encoded)</h2> 8519261079SEd Maste<pre id="assertresultsshsigb64" style="color: #008; font-family: monospace;"></pre> 8619261079SEd Maste</span> 8719261079SEd Maste</body> 8819261079SEd Maste<script> 8919261079SEd Maste// ------------------------------------------------------------------ 9019261079SEd Maste// a crappy CBOR decoder - 20200401 djm@openbsd.org 9119261079SEd Maste 9219261079SEd Mastevar CBORDecode = function(buffer) { 9319261079SEd Maste this.buf = buffer 9419261079SEd Maste this.v = new DataView(buffer) 9519261079SEd Maste this.offset = 0 9619261079SEd Maste} 9719261079SEd Maste 9819261079SEd MasteCBORDecode.prototype.empty = function() { 9919261079SEd Maste return this.offset >= this.buf.byteLength 10019261079SEd Maste} 10119261079SEd Maste 10219261079SEd MasteCBORDecode.prototype.getU8 = function() { 10319261079SEd Maste let r = this.v.getUint8(this.offset) 10419261079SEd Maste this.offset += 1 10519261079SEd Maste return r 10619261079SEd Maste} 10719261079SEd Maste 10819261079SEd MasteCBORDecode.prototype.getU16 = function() { 10919261079SEd Maste let r = this.v.getUint16(this.offset) 11019261079SEd Maste this.offset += 2 11119261079SEd Maste return r 11219261079SEd Maste} 11319261079SEd Maste 11419261079SEd MasteCBORDecode.prototype.getU32 = function() { 11519261079SEd Maste let r = this.v.getUint32(this.offset) 11619261079SEd Maste this.offset += 4 11719261079SEd Maste return r 11819261079SEd Maste} 11919261079SEd Maste 12019261079SEd MasteCBORDecode.prototype.getU64 = function() { 12119261079SEd Maste let r = this.v.getUint64(this.offset) 12219261079SEd Maste this.offset += 8 12319261079SEd Maste return r 12419261079SEd Maste} 12519261079SEd Maste 12619261079SEd MasteCBORDecode.prototype.getCBORTypeLen = function() { 12719261079SEd Maste let tl, t, l 12819261079SEd Maste tl = this.getU8() 12919261079SEd Maste t = (tl & 0xe0) >> 5 13019261079SEd Maste l = tl & 0x1f 13119261079SEd Maste return [t, this.decodeInteger(l)] 13219261079SEd Maste} 13319261079SEd Maste 13419261079SEd MasteCBORDecode.prototype.decodeInteger = function(len) { 13519261079SEd Maste switch (len) { 13619261079SEd Maste case 0x18: return this.getU8() 13719261079SEd Maste case 0x19: return this.getU16() 13819261079SEd Maste case 0x20: return this.getU32() 13919261079SEd Maste case 0x21: return this.getU64() 14019261079SEd Maste default: 14119261079SEd Maste if (len <= 23) { 14219261079SEd Maste return len 14319261079SEd Maste } 14419261079SEd Maste throw new Error("Unsupported int type 0x" + len.toString(16)) 14519261079SEd Maste } 14619261079SEd Maste} 14719261079SEd Maste 14819261079SEd MasteCBORDecode.prototype.decodeNegint = function(len) { 14919261079SEd Maste let r = -(this.decodeInteger(len) + 1) 15019261079SEd Maste return r 15119261079SEd Maste} 15219261079SEd Maste 15319261079SEd MasteCBORDecode.prototype.decodeByteString = function(len) { 15419261079SEd Maste let r = this.buf.slice(this.offset, this.offset + len) 15519261079SEd Maste this.offset += len 15619261079SEd Maste return r 15719261079SEd Maste} 15819261079SEd Maste 15919261079SEd MasteCBORDecode.prototype.decodeTextString = function(len) { 16019261079SEd Maste let u8dec = new TextDecoder('utf-8') 16119261079SEd Maste r = u8dec.decode(this.decodeByteString(len)) 16219261079SEd Maste return r 16319261079SEd Maste} 16419261079SEd Maste 16519261079SEd MasteCBORDecode.prototype.decodeArray = function(len, level) { 16619261079SEd Maste let r = [] 16719261079SEd Maste for (let i = 0; i < len; i++) { 16819261079SEd Maste let v = this.decodeInternal(level) 16919261079SEd Maste r.push(v) 17019261079SEd Maste // console.log("decodeArray level " + level.toString() + " index " + i.toString() + " value " + JSON.stringify(v)) 17119261079SEd Maste } 17219261079SEd Maste return r 17319261079SEd Maste} 17419261079SEd Maste 17519261079SEd MasteCBORDecode.prototype.decodeMap = function(len, level) { 17619261079SEd Maste let r = {} 17719261079SEd Maste for (let i = 0; i < len; i++) { 17819261079SEd Maste let k = this.decodeInternal(level) 17919261079SEd Maste let v = this.decodeInternal(level) 18019261079SEd Maste r[k] = v 18119261079SEd Maste // console.log("decodeMap level " + level.toString() + " key " + k.toString() + " value " + JSON.stringify(v)) 18219261079SEd Maste // XXX check string keys, duplicates 18319261079SEd Maste } 18419261079SEd Maste return r 18519261079SEd Maste} 18619261079SEd Maste 18719261079SEd MasteCBORDecode.prototype.decodePrimitive = function(t) { 18819261079SEd Maste switch (t) { 18919261079SEd Maste case 20: return false 19019261079SEd Maste case 21: return true 19119261079SEd Maste case 22: return null 19219261079SEd Maste case 23: return undefined 19319261079SEd Maste default: 19419261079SEd Maste throw new Error("Unsupported primitive 0x" + t.toString(2)) 19519261079SEd Maste } 19619261079SEd Maste} 19719261079SEd Maste 19819261079SEd MasteCBORDecode.prototype.decodeInternal = function(level) { 19919261079SEd Maste if (level > 256) { 20019261079SEd Maste throw new Error("CBOR nesting too deep") 20119261079SEd Maste } 20219261079SEd Maste let t, l, r 20319261079SEd Maste [t, l] = this.getCBORTypeLen() 20419261079SEd Maste // console.log("decode level " + level.toString() + " type " + t.toString() + " len " + l.toString()) 20519261079SEd Maste switch (t) { 20619261079SEd Maste case 0: 20719261079SEd Maste r = this.decodeInteger(l) 20819261079SEd Maste break 20919261079SEd Maste case 1: 21019261079SEd Maste r = this.decodeNegint(l) 21119261079SEd Maste break 21219261079SEd Maste case 2: 21319261079SEd Maste r = this.decodeByteString(l) 21419261079SEd Maste break 21519261079SEd Maste case 3: 21619261079SEd Maste r = this.decodeTextString(l) 21719261079SEd Maste break 21819261079SEd Maste case 4: 21919261079SEd Maste r = this.decodeArray(l, level + 1) 22019261079SEd Maste break 22119261079SEd Maste case 5: 22219261079SEd Maste r = this.decodeMap(l, level + 1) 22319261079SEd Maste break 22419261079SEd Maste case 6: 22519261079SEd Maste console.log("XXX ignored semantic tag " + this.decodeInteger(l).toString()) 22619261079SEd Maste break; 22719261079SEd Maste case 7: 22819261079SEd Maste r = this.decodePrimitive(l) 22919261079SEd Maste break 23019261079SEd Maste default: 23119261079SEd Maste throw new Error("Unsupported type 0x" + t.toString(2) + " len " + l.toString()) 23219261079SEd Maste } 23319261079SEd Maste // console.log("decode level " + level.toString() + " value " + JSON.stringify(r)) 23419261079SEd Maste return r 23519261079SEd Maste} 23619261079SEd Maste 23719261079SEd MasteCBORDecode.prototype.decode = function() { 23819261079SEd Maste return this.decodeInternal(0) 23919261079SEd Maste} 24019261079SEd Maste 24119261079SEd Maste// ------------------------------------------------------------------ 24219261079SEd Maste// a crappy SSH message packer - 20200401 djm@openbsd.org 24319261079SEd Maste 24419261079SEd Mastevar SSHMSG = function() { 24519261079SEd Maste this.r = [] 24619261079SEd Maste} 24719261079SEd Maste 24819261079SEd MasteSSHMSG.prototype.length = function() { 24919261079SEd Maste let len = 0 25019261079SEd Maste for (buf of this.r) { 25119261079SEd Maste len += buf.length 25219261079SEd Maste } 25319261079SEd Maste return len 25419261079SEd Maste} 25519261079SEd Maste 25619261079SEd MasteSSHMSG.prototype.serialise = function() { 25719261079SEd Maste let r = new ArrayBuffer(this.length()) 25819261079SEd Maste let v = new Uint8Array(r) 25919261079SEd Maste let offset = 0 26019261079SEd Maste for (buf of this.r) { 26119261079SEd Maste v.set(buf, offset) 26219261079SEd Maste offset += buf.length 26319261079SEd Maste } 26419261079SEd Maste if (offset != r.byteLength) { 26519261079SEd Maste throw new Error("djm can't count") 26619261079SEd Maste } 26719261079SEd Maste return r 26819261079SEd Maste} 26919261079SEd Maste 27019261079SEd MasteSSHMSG.prototype.serialiseBase64 = function(v) { 27119261079SEd Maste let b = this.serialise() 27219261079SEd Maste return btoa(String.fromCharCode(...new Uint8Array(b))); 27319261079SEd Maste} 27419261079SEd Maste 27519261079SEd MasteSSHMSG.prototype.putU8 = function(v) { 27619261079SEd Maste this.r.push(new Uint8Array([v])) 27719261079SEd Maste} 27819261079SEd Maste 27919261079SEd MasteSSHMSG.prototype.putU32 = function(v) { 28019261079SEd Maste this.r.push(new Uint8Array([ 28119261079SEd Maste (v >> 24) & 0xff, 28219261079SEd Maste (v >> 16) & 0xff, 28319261079SEd Maste (v >> 8) & 0xff, 28419261079SEd Maste (v & 0xff) 28519261079SEd Maste ])) 28619261079SEd Maste} 28719261079SEd Maste 28819261079SEd MasteSSHMSG.prototype.put = function(v) { 28919261079SEd Maste this.r.push(new Uint8Array(v)) 29019261079SEd Maste} 29119261079SEd Maste 29219261079SEd MasteSSHMSG.prototype.putStringRaw = function(v) { 29319261079SEd Maste let enc = new TextEncoder(); 29419261079SEd Maste let venc = enc.encode(v) 29519261079SEd Maste this.put(venc) 29619261079SEd Maste} 29719261079SEd Maste 29819261079SEd MasteSSHMSG.prototype.putString = function(v) { 29919261079SEd Maste let enc = new TextEncoder(); 30019261079SEd Maste let venc = enc.encode(v) 30119261079SEd Maste this.putU32(venc.length) 30219261079SEd Maste this.put(venc) 30319261079SEd Maste} 30419261079SEd Maste 30519261079SEd MasteSSHMSG.prototype.putSSHMSG = function(v) { 30619261079SEd Maste let msg = v.serialise() 30719261079SEd Maste this.putU32(msg.byteLength) 30819261079SEd Maste this.put(msg) 30919261079SEd Maste} 31019261079SEd Maste 31119261079SEd MasteSSHMSG.prototype.putBytes = function(v) { 31219261079SEd Maste this.putU32(v.byteLength) 31319261079SEd Maste this.put(v) 31419261079SEd Maste} 31519261079SEd Maste 31619261079SEd MasteSSHMSG.prototype.putECPoint = function(x, y) { 31719261079SEd Maste let x8 = new Uint8Array(x) 31819261079SEd Maste let y8 = new Uint8Array(y) 31919261079SEd Maste this.putU32(1 + x8.length + y8.length) 32019261079SEd Maste this.putU8(0x04) // Uncompressed point format. 32119261079SEd Maste this.put(x8) 32219261079SEd Maste this.put(y8) 32319261079SEd Maste} 32419261079SEd Maste 32519261079SEd Maste// ------------------------------------------------------------------ 32619261079SEd Maste// webauthn to SSH glue - djm@openbsd.org 20200408 32719261079SEd Maste 32819261079SEd Mastefunction error(msg, ...args) { 32919261079SEd Maste document.getElementById("error").innerText = msg 33019261079SEd Maste console.log(msg) 33119261079SEd Maste for (const arg of args) { 33219261079SEd Maste console.dir(arg) 33319261079SEd Maste } 33419261079SEd Maste} 33519261079SEd Mastefunction hexdump(buf) { 33619261079SEd Maste const hex = Array.from(new Uint8Array(buf)).map( 33719261079SEd Maste b => b.toString(16).padStart(2, "0")) 33819261079SEd Maste const fmt = new Array() 33919261079SEd Maste for (let i = 0; i < hex.length; i++) { 34019261079SEd Maste if ((i % 16) == 0) { 34119261079SEd Maste // Prepend length every 16 bytes. 34219261079SEd Maste fmt.push(i.toString(16).padStart(4, "0")) 34319261079SEd Maste fmt.push(" ") 34419261079SEd Maste } 34519261079SEd Maste fmt.push(hex[i]) 34619261079SEd Maste fmt.push(" ") 34719261079SEd Maste if ((i % 16) == 15) { 34819261079SEd Maste fmt.push("\n") 34919261079SEd Maste } 35019261079SEd Maste } 35119261079SEd Maste return fmt.join("") 35219261079SEd Maste} 35319261079SEd Mastefunction enrollform_submit(event) { 35419261079SEd Maste event.preventDefault(); 35519261079SEd Maste console.log("submitted") 35619261079SEd Maste username = event.target.elements.username.value 35719261079SEd Maste if (username === "") { 35819261079SEd Maste error("no username specified") 35919261079SEd Maste return false 36019261079SEd Maste } 36119261079SEd Maste enrollStart(username) 36219261079SEd Maste} 36319261079SEd Mastefunction enrollStart(username) { 36419261079SEd Maste let challenge = new Uint8Array(32) 36519261079SEd Maste window.crypto.getRandomValues(challenge) 36619261079SEd Maste let userid = new Uint8Array(8) 36719261079SEd Maste window.crypto.getRandomValues(userid) 36819261079SEd Maste 36919261079SEd Maste console.log("challenge:" + btoa(challenge)) 37019261079SEd Maste console.log("userid:" + btoa(userid)) 37119261079SEd Maste 37219261079SEd Maste let pkopts = { 37319261079SEd Maste challenge: challenge, 37419261079SEd Maste rp: { 375*1323ec57SEd Maste name: window.location.host, 376*1323ec57SEd Maste id: window.location.host, 37719261079SEd Maste }, 37819261079SEd Maste user: { 37919261079SEd Maste id: userid, 38019261079SEd Maste name: username, 38119261079SEd Maste displayName: username, 38219261079SEd Maste }, 38319261079SEd Maste authenticatorSelection: { 38419261079SEd Maste authenticatorAttachment: "cross-platform", 38519261079SEd Maste userVerification: "discouraged", 38619261079SEd Maste }, 38719261079SEd Maste pubKeyCredParams: [{alg: -7, type: "public-key"}], // ES256 38819261079SEd Maste timeout: 30 * 1000, 38919261079SEd Maste }; 39019261079SEd Maste console.dir(pkopts) 39119261079SEd Maste window.enrollOpts = pkopts 39219261079SEd Maste let credpromise = navigator.credentials.create({ publicKey: pkopts }); 39319261079SEd Maste credpromise.then(enrollSuccess, enrollFailure) 39419261079SEd Maste} 39519261079SEd Mastefunction enrollFailure(result) { 39619261079SEd Maste error("Enroll failed", result) 39719261079SEd Maste} 39819261079SEd Mastefunction enrollSuccess(result) { 39919261079SEd Maste console.log("Enroll succeeded") 40019261079SEd Maste console.dir(result) 40119261079SEd Maste window.enrollResult = result 40219261079SEd Maste document.getElementById("enrollresult").style.visibility = "visible" 40319261079SEd Maste 40419261079SEd Maste // Show the clientData 40519261079SEd Maste let u8dec = new TextDecoder('utf-8') 40619261079SEd Maste clientData = u8dec.decode(result.response.clientDataJSON) 40719261079SEd Maste document.getElementById("enrollresultjson").innerText = clientData 40819261079SEd Maste 40919261079SEd Maste // Show the raw key handle. 41019261079SEd Maste document.getElementById("keyhandle").innerText = hexdump(result.rawId) 41119261079SEd Maste 41219261079SEd Maste // Decode and show the attestationObject 41319261079SEd Maste document.getElementById("enrollresultraw").innerText = hexdump(result.response.attestationObject) 41419261079SEd Maste let aod = new CBORDecode(result.response.attestationObject) 41519261079SEd Maste let attestationObject = aod.decode() 41619261079SEd Maste console.log("attestationObject") 41719261079SEd Maste console.dir(attestationObject) 41819261079SEd Maste document.getElementById("enrollresultattestobj").innerText = JSON.stringify(attestationObject) 41919261079SEd Maste 42019261079SEd Maste // Decode and show the authData 42119261079SEd Maste document.getElementById("enrollresultauthdataraw").innerText = hexdump(attestationObject.authData) 42219261079SEd Maste let authData = decodeAuthenticatorData(attestationObject.authData, true) 42319261079SEd Maste console.log("authData") 42419261079SEd Maste console.dir(authData) 42519261079SEd Maste window.enrollAuthData = authData 42619261079SEd Maste document.getElementById("enrollresultauthdata").innerText = JSON.stringify(authData) 42719261079SEd Maste 42819261079SEd Maste // Reformat the pubkey as a SSH key for easy verification 42919261079SEd Maste window.rawKey = reformatPubkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id) 43019261079SEd Maste console.log("SSH pubkey blob") 43119261079SEd Maste console.dir(window.rawKey) 43219261079SEd Maste document.getElementById("enrollresultpkblob").innerText = hexdump(window.rawKey) 43319261079SEd Maste let pk64 = btoa(String.fromCharCode(...new Uint8Array(window.rawKey))); 43419261079SEd Maste let pk = "sk-ecdsa-sha2-nistp256@openssh.com " + pk64 43519261079SEd Maste document.getElementById("enrollresultpk").innerText = pk 43619261079SEd Maste 43719261079SEd Maste // Format a private key too. 43819261079SEd Maste flags = 0x01 // SSH_SK_USER_PRESENCE_REQD 43919261079SEd Maste window.rawPrivkey = reformatPrivkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id, result.rawId, flags) 44019261079SEd Maste let privkeyFileBlob = privkeyFile(window.rawKey, window.rawPrivkey, window.enrollOpts.user.name, window.enrollOpts.rp.id) 44119261079SEd Maste let privk64 = btoa(String.fromCharCode(...new Uint8Array(privkeyFileBlob))); 44219261079SEd Maste let privkey = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + wrapString(privk64, 70) + "-----END OPENSSH PRIVATE KEY-----\n" 44319261079SEd Maste document.getElementById("enrollresultprivkey").innerText = privkey 44419261079SEd Maste 44519261079SEd Maste // Success: show the assertion form. 44619261079SEd Maste document.getElementById("assertsection").style.visibility = "visible" 44719261079SEd Maste} 44819261079SEd Maste 44919261079SEd Mastefunction decodeAuthenticatorData(authData, expectCred) { 45019261079SEd Maste let r = new Object() 45119261079SEd Maste let v = new DataView(authData) 45219261079SEd Maste 45319261079SEd Maste r.rpIdHash = authData.slice(0, 32) 45419261079SEd Maste r.flags = v.getUint8(32) 45519261079SEd Maste r.signCount = v.getUint32(33) 45619261079SEd Maste 45719261079SEd Maste // Decode attestedCredentialData if present. 45819261079SEd Maste let offset = 37 45919261079SEd Maste let acd = new Object() 46019261079SEd Maste if (expectCred) { 46119261079SEd Maste acd.aaguid = authData.slice(offset, offset+16) 46219261079SEd Maste offset += 16 46319261079SEd Maste let credentialIdLength = v.getUint16(offset) 46419261079SEd Maste offset += 2 46519261079SEd Maste acd.credentialIdLength = credentialIdLength 46619261079SEd Maste acd.credentialId = authData.slice(offset, offset+credentialIdLength) 46719261079SEd Maste offset += credentialIdLength 46819261079SEd Maste r.attestedCredentialData = acd 46919261079SEd Maste } 47019261079SEd Maste console.log("XXXXX " + offset.toString()) 47119261079SEd Maste let pubkeyrest = authData.slice(offset, authData.byteLength) 47219261079SEd Maste let pkdecode = new CBORDecode(pubkeyrest) 47319261079SEd Maste if (expectCred) { 47419261079SEd Maste // XXX unsafe: doesn't mandate COSE canonical format. 47519261079SEd Maste acd.credentialPublicKey = pkdecode.decode() 47619261079SEd Maste } 47719261079SEd Maste if (!pkdecode.empty()) { 47819261079SEd Maste // Decode extensions if present. 47919261079SEd Maste r.extensions = pkdecode.decode() 48019261079SEd Maste } 48119261079SEd Maste return r 48219261079SEd Maste} 48319261079SEd Maste 48419261079SEd Mastefunction wrapString(s, l) { 48519261079SEd Maste ret = "" 48619261079SEd Maste for (i = 0; i < s.length; i += l) { 48719261079SEd Maste ret += s.slice(i, i + l) + "\n" 48819261079SEd Maste } 48919261079SEd Maste return ret 49019261079SEd Maste} 49119261079SEd Maste 49219261079SEd Mastefunction checkPubkey(pk) { 49319261079SEd Maste // pk is in COSE format. We only care about a tiny subset. 49419261079SEd Maste if (pk[1] != 2) { 49519261079SEd Maste console.dir(pk) 49619261079SEd Maste throw new Error("pubkey is not EC") 49719261079SEd Maste } 49819261079SEd Maste if (pk[-1] != 1) { 49919261079SEd Maste throw new Error("pubkey is not in P256") 50019261079SEd Maste } 50119261079SEd Maste if (pk[3] != -7) { 50219261079SEd Maste throw new Error("pubkey is not ES256") 50319261079SEd Maste } 50419261079SEd Maste if (pk[-2].byteLength != 32 || pk[-3].byteLength != 32) { 50519261079SEd Maste throw new Error("pubkey EC coords have bad length") 50619261079SEd Maste } 50719261079SEd Maste} 50819261079SEd Maste 50919261079SEd Mastefunction reformatPubkey(pk, rpid) { 51019261079SEd Maste checkPubkey(pk) 51119261079SEd Maste let msg = new SSHMSG() 51219261079SEd Maste msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type 51319261079SEd Maste msg.putString("nistp256") // Key curve 51419261079SEd Maste msg.putECPoint(pk[-2], pk[-3]) // EC key 51519261079SEd Maste msg.putString(rpid) // RP ID 51619261079SEd Maste return msg.serialise() 51719261079SEd Maste} 51819261079SEd Maste 51919261079SEd Mastefunction reformatPrivkey(pk, rpid, kh, flags) { 52019261079SEd Maste checkPubkey(pk) 52119261079SEd Maste let msg = new SSHMSG() 52219261079SEd Maste msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type 52319261079SEd Maste msg.putString("nistp256") // Key curve 52419261079SEd Maste msg.putECPoint(pk[-2], pk[-3]) // EC key 52519261079SEd Maste msg.putString(rpid) // RP ID 52619261079SEd Maste msg.putU8(flags) // flags 52719261079SEd Maste msg.putBytes(kh) // handle 52819261079SEd Maste msg.putString("") // reserved 52919261079SEd Maste return msg.serialise() 53019261079SEd Maste} 53119261079SEd Maste 53219261079SEd Mastefunction privkeyFile(pub, priv, user, rp) { 53319261079SEd Maste let innerMsg = new SSHMSG() 53419261079SEd Maste innerMsg.putU32(0xdeadbeef) // check byte 53519261079SEd Maste innerMsg.putU32(0xdeadbeef) // check byte 53619261079SEd Maste innerMsg.put(priv) // privkey 53719261079SEd Maste innerMsg.putString("webauthn.html " + user + "@" + rp) // comment 53819261079SEd Maste // Pad to cipher blocksize (8). 53919261079SEd Maste p = 1 54019261079SEd Maste while (innerMsg.length() % 8 != 0) { 54119261079SEd Maste innerMsg.putU8(p++) 54219261079SEd Maste } 54319261079SEd Maste let msg = new SSHMSG() 54419261079SEd Maste msg.putStringRaw("openssh-key-v1") // Magic 54519261079SEd Maste msg.putU8(0) // \0 terminate 54619261079SEd Maste msg.putString("none") // cipher 54719261079SEd Maste msg.putString("none") // KDF 54819261079SEd Maste msg.putString("") // KDF options 54919261079SEd Maste msg.putU32(1) // nkeys 55019261079SEd Maste msg.putBytes(pub) // pubkey 55119261079SEd Maste msg.putSSHMSG(innerMsg) // inner 55219261079SEd Maste //msg.put(innerMsg.serialise()) // inner 55319261079SEd Maste return msg.serialise() 55419261079SEd Maste} 55519261079SEd Maste 55619261079SEd Masteasync function assertform_submit(event) { 55719261079SEd Maste event.preventDefault(); 55819261079SEd Maste console.log("submitted") 55919261079SEd Maste message = event.target.elements.message.value 56019261079SEd Maste if (message === "") { 56119261079SEd Maste error("no message specified") 56219261079SEd Maste return false 56319261079SEd Maste } 56419261079SEd Maste let enc = new TextEncoder() 56519261079SEd Maste let encmsg = enc.encode(message) 56619261079SEd Maste window.assertSignRaw = !event.target.elements.message_sshsig.checked 56719261079SEd Maste console.log("using sshsig ", !window.assertSignRaw) 56819261079SEd Maste if (window.assertSignRaw) { 56919261079SEd Maste assertStart(encmsg) 57019261079SEd Maste return 57119261079SEd Maste } 57219261079SEd Maste // Format a sshsig-style message. 57319261079SEd Maste window.sigHashAlg = "sha512" 57419261079SEd Maste let msghash = await crypto.subtle.digest("SHA-512", encmsg); 57519261079SEd Maste console.log("raw message hash") 57619261079SEd Maste console.dir(msghash) 57719261079SEd Maste window.sigNamespace = event.target.elements.message_namespace.value 57819261079SEd Maste let sigbuf = new SSHMSG() 57919261079SEd Maste sigbuf.put(enc.encode("SSHSIG")) 58019261079SEd Maste sigbuf.putString(window.sigNamespace) 58119261079SEd Maste sigbuf.putU32(0) // Reserved string 58219261079SEd Maste sigbuf.putString(window.sigHashAlg) 58319261079SEd Maste sigbuf.putBytes(msghash) 58419261079SEd Maste let msg = sigbuf.serialise() 58519261079SEd Maste console.log("sigbuf") 58619261079SEd Maste console.dir(msg) 58719261079SEd Maste assertStart(msg) 58819261079SEd Maste} 58919261079SEd Maste 59019261079SEd Mastefunction assertStart(message) { 59119261079SEd Maste let assertReqOpts = { 59219261079SEd Maste challenge: message, 593*1323ec57SEd Maste rpId: window.location.host, 59419261079SEd Maste allowCredentials: [{ 59519261079SEd Maste type: 'public-key', 59619261079SEd Maste id: window.enrollResult.rawId, 59719261079SEd Maste }], 59819261079SEd Maste userVerification: "discouraged", 59919261079SEd Maste timeout: (30 * 1000), 60019261079SEd Maste } 60119261079SEd Maste console.log("assertReqOpts") 60219261079SEd Maste console.dir(assertReqOpts) 60319261079SEd Maste window.assertReqOpts = assertReqOpts 60419261079SEd Maste let assertpromise = navigator.credentials.get({ 60519261079SEd Maste publicKey: assertReqOpts 60619261079SEd Maste }); 60719261079SEd Maste assertpromise.then(assertSuccess, assertFailure) 60819261079SEd Maste} 60919261079SEd Mastefunction assertFailure(result) { 61019261079SEd Maste error("Assertion failed", result) 61119261079SEd Maste} 61219261079SEd Mastefunction linewrap(s) { 61319261079SEd Maste const linelen = 70 61419261079SEd Maste let ret = "" 61519261079SEd Maste for (let i = 0; i < s.length; i += linelen) { 61619261079SEd Maste end = i + linelen 61719261079SEd Maste if (end > s.length) { 61819261079SEd Maste end = s.length 61919261079SEd Maste } 62019261079SEd Maste if (i > 0) { 62119261079SEd Maste ret += "\n" 62219261079SEd Maste } 62319261079SEd Maste ret += s.slice(i, end) 62419261079SEd Maste } 62519261079SEd Maste return ret + "\n" 62619261079SEd Maste} 62719261079SEd Mastefunction assertSuccess(result) { 62819261079SEd Maste console.log("Assertion succeeded") 62919261079SEd Maste console.dir(result) 63019261079SEd Maste window.assertResult = result 63119261079SEd Maste document.getElementById("assertresult").style.visibility = "visible" 63219261079SEd Maste 63319261079SEd Maste // show the clientData. 63419261079SEd Maste let u8dec = new TextDecoder('utf-8') 63519261079SEd Maste clientData = u8dec.decode(result.response.clientDataJSON) 63619261079SEd Maste document.getElementById("assertresultjson").innerText = clientData 63719261079SEd Maste 63819261079SEd Maste // show the signature. 63919261079SEd Maste document.getElementById("assertresultsigraw").innerText = hexdump(result.response.signature) 64019261079SEd Maste 64119261079SEd Maste // decode and show the authData. 64219261079SEd Maste document.getElementById("assertresultauthdataraw").innerText = hexdump(result.response.authenticatorData) 64319261079SEd Maste authData = decodeAuthenticatorData(result.response.authenticatorData, false) 64419261079SEd Maste document.getElementById("assertresultauthdata").innerText = JSON.stringify(authData) 64519261079SEd Maste 64619261079SEd Maste // Parse and reformat the signature to an SSH style signature. 64719261079SEd Maste let sshsig = reformatSignature(result.response.signature, clientData, authData) 64819261079SEd Maste document.getElementById("assertresultsshsigraw").innerText = hexdump(sshsig) 64919261079SEd Maste let sig64 = btoa(String.fromCharCode(...new Uint8Array(sshsig))); 65019261079SEd Maste if (window.assertSignRaw) { 65119261079SEd Maste document.getElementById("assertresultsshsigb64").innerText = sig64 65219261079SEd Maste } else { 65319261079SEd Maste document.getElementById("assertresultsshsigb64").innerText = 65419261079SEd Maste "-----BEGIN SSH SIGNATURE-----\n" + linewrap(sig64) + 65519261079SEd Maste "-----END SSH SIGNATURE-----\n"; 65619261079SEd Maste } 65719261079SEd Maste} 65819261079SEd Maste 65919261079SEd Mastefunction reformatSignature(sig, clientData, authData) { 66019261079SEd Maste if (sig.byteLength < 2) { 66119261079SEd Maste throw new Error("signature is too short") 66219261079SEd Maste } 66319261079SEd Maste let offset = 0 66419261079SEd Maste let v = new DataView(sig) 66519261079SEd Maste // Expect an ASN.1 SEQUENCE that exactly spans the signature. 66619261079SEd Maste if (v.getUint8(offset) != 0x30) { 66719261079SEd Maste throw new Error("signature not an ASN.1 sequence") 66819261079SEd Maste } 66919261079SEd Maste offset++ 67019261079SEd Maste let seqlen = v.getUint8(offset) 67119261079SEd Maste offset++ 67219261079SEd Maste if ((seqlen & 0x80) != 0 || seqlen != sig.byteLength - offset) { 67319261079SEd Maste throw new Error("signature has unexpected length " + seqlen.toString() + " vs expected " + (sig.byteLength - offset).toString()) 67419261079SEd Maste } 67519261079SEd Maste 67619261079SEd Maste // Parse 'r' INTEGER value. 67719261079SEd Maste if (v.getUint8(offset) != 0x02) { 67819261079SEd Maste throw new Error("signature r not an ASN.1 integer") 67919261079SEd Maste } 68019261079SEd Maste offset++ 68119261079SEd Maste let rlen = v.getUint8(offset) 68219261079SEd Maste offset++ 68319261079SEd Maste if ((rlen & 0x80) != 0 || rlen > sig.byteLength - offset) { 68419261079SEd Maste throw new Error("signature r has unexpected length " + rlen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) 68519261079SEd Maste } 68619261079SEd Maste let r = sig.slice(offset, offset + rlen) 68719261079SEd Maste offset += rlen 68819261079SEd Maste console.log("sig_r") 68919261079SEd Maste console.dir(r) 69019261079SEd Maste 69119261079SEd Maste // Parse 's' INTEGER value. 69219261079SEd Maste if (v.getUint8(offset) != 0x02) { 69319261079SEd Maste throw new Error("signature r not an ASN.1 integer") 69419261079SEd Maste } 69519261079SEd Maste offset++ 69619261079SEd Maste let slen = v.getUint8(offset) 69719261079SEd Maste offset++ 69819261079SEd Maste if ((slen & 0x80) != 0 || slen > sig.byteLength - offset) { 69919261079SEd Maste throw new Error("signature s has unexpected length " + slen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) 70019261079SEd Maste } 70119261079SEd Maste let s = sig.slice(offset, offset + slen) 70219261079SEd Maste console.log("sig_s") 70319261079SEd Maste console.dir(s) 70419261079SEd Maste offset += slen 70519261079SEd Maste 70619261079SEd Maste if (offset != sig.byteLength) { 70719261079SEd Maste throw new Error("unexpected final offset during signature parsing " + offset.toString() + " expected " + sig.byteLength.toString()) 70819261079SEd Maste } 70919261079SEd Maste 71019261079SEd Maste // Reformat as an SSH signature. 71119261079SEd Maste let clientDataParsed = JSON.parse(clientData) 71219261079SEd Maste let innersig = new SSHMSG() 71319261079SEd Maste innersig.putBytes(r) 71419261079SEd Maste innersig.putBytes(s) 71519261079SEd Maste 71619261079SEd Maste let rawsshsig = new SSHMSG() 71719261079SEd Maste rawsshsig.putString("webauthn-sk-ecdsa-sha2-nistp256@openssh.com") 71819261079SEd Maste rawsshsig.putSSHMSG(innersig) 71919261079SEd Maste rawsshsig.putU8(authData.flags) 72019261079SEd Maste rawsshsig.putU32(authData.signCount) 72119261079SEd Maste rawsshsig.putString(clientDataParsed.origin) 72219261079SEd Maste rawsshsig.putString(clientData) 72319261079SEd Maste if (authData.extensions == undefined) { 72419261079SEd Maste rawsshsig.putU32(0) 72519261079SEd Maste } else { 72619261079SEd Maste rawsshsig.putBytes(authData.extensions) 72719261079SEd Maste } 72819261079SEd Maste 72919261079SEd Maste if (window.assertSignRaw) { 73019261079SEd Maste return rawsshsig.serialise() 73119261079SEd Maste } 73219261079SEd Maste // Format as SSHSIG. 73319261079SEd Maste let enc = new TextEncoder() 73419261079SEd Maste let sshsig = new SSHMSG() 73519261079SEd Maste sshsig.put(enc.encode("SSHSIG")) 73619261079SEd Maste sshsig.putU32(0x01) // Signature version. 73719261079SEd Maste sshsig.putBytes(window.rawKey) 73819261079SEd Maste sshsig.putString(window.sigNamespace) 73919261079SEd Maste sshsig.putU32(0) // Reserved string 74019261079SEd Maste sshsig.putString(window.sigHashAlg) 74119261079SEd Maste sshsig.putBytes(rawsshsig.serialise()) 74219261079SEd Maste return sshsig.serialise() 74319261079SEd Maste} 74419261079SEd Maste 74519261079SEd Mastefunction toggleNamespaceVisibility() { 74619261079SEd Maste const assertsigtype = document.getElementById('message_sshsig'); 74719261079SEd Maste const assertsignamespace = document.getElementById('message_namespace'); 74819261079SEd Maste assertsignamespace.disabled = !assertsigtype.checked; 74919261079SEd Maste} 75019261079SEd Maste 75119261079SEd Mastefunction init() { 75219261079SEd Maste if (document.location.protocol != "https:") { 75319261079SEd Maste error("This page must be loaded via https") 75419261079SEd Maste const assertsubmit = document.getElementById('assertsubmit') 75519261079SEd Maste assertsubmit.disabled = true 75619261079SEd Maste } 75719261079SEd Maste const enrollform = document.getElementById('enrollform'); 75819261079SEd Maste enrollform.addEventListener('submit', enrollform_submit); 75919261079SEd Maste const assertform = document.getElementById('assertform'); 76019261079SEd Maste assertform.addEventListener('submit', assertform_submit); 76119261079SEd Maste const assertsigtype = document.getElementById('message_sshsig'); 76219261079SEd Maste assertsigtype.onclick = toggleNamespaceVisibility; 76319261079SEd Maste} 76419261079SEd Maste</script> 76519261079SEd Maste 76619261079SEd Maste</html> 767