1*19261079SEd Maste<html> 2*19261079SEd Maste<head> 3*19261079SEd Maste<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 4*19261079SEd Maste<title>webauthn test</title> 5*19261079SEd Maste</head> 6*19261079SEd Maste<body onload="init()"> 7*19261079SEd Maste<h1>webauthn test</h1> 8*19261079SEd Maste<p> 9*19261079SEd MasteThis is a demo/test page for generating FIDO keys and signatures in SSH 10*19261079SEd Masteformats. The page initially displays a form to generate a FIDO key and 11*19261079SEd Masteconvert it to a SSH public key. 12*19261079SEd Maste</p> 13*19261079SEd Maste<p> 14*19261079SEd MasteOnce a key has been generated, an additional form will be displayed to 15*19261079SEd Masteallow signing of data using the just-generated key. The data may be signed 16*19261079SEd Masteas either a raw SSH signature or wrapped in a sshsig message (the latter is 17*19261079SEd Masteeasier to test using command-line tools. 18*19261079SEd Maste</p> 19*19261079SEd Maste<p> 20*19261079SEd MasteLots of debugging is printed along the way. 21*19261079SEd Maste</p> 22*19261079SEd Maste<h2>Enroll</h2> 23*19261079SEd Maste<span id="error" style="color: #800; font-weight: bold; font-size: 150%;"></span> 24*19261079SEd Maste<form id="enrollform"> 25*19261079SEd Maste<table> 26*19261079SEd Maste<tr> 27*19261079SEd Maste<td><b>Username:</b></td> 28*19261079SEd Maste<td><input id="username" type="text" size="20" name="user" value="test" /></td> 29*19261079SEd Maste</tr> 30*19261079SEd Maste<tr><td></td><td><input id="assertsubmit" type="submit" value="submit" /></td></tr> 31*19261079SEd Maste</table> 32*19261079SEd Maste</form> 33*19261079SEd Maste<span id="enrollresult" style="visibility: hidden;"> 34*19261079SEd Maste<h2>clientData</h2> 35*19261079SEd Maste<pre id="enrollresultjson" style="color: #008; font-family: monospace;"></pre> 36*19261079SEd Maste<h2>attestationObject raw</h2> 37*19261079SEd Maste<pre id="enrollresultraw" style="color: #008; font-family: monospace;"></pre> 38*19261079SEd Maste<h2>attestationObject</h2> 39*19261079SEd Maste<pre id="enrollresultattestobj" style="color: #008; font-family: monospace;"></pre> 40*19261079SEd Maste<h2>key handle</h2> 41*19261079SEd Maste<pre id="keyhandle" style="color: #008; font-family: monospace;"></pre> 42*19261079SEd Maste<h2>authData raw</h2> 43*19261079SEd Maste<pre id="enrollresultauthdataraw" style="color: #008; font-family: monospace;"></pre> 44*19261079SEd Maste<h2>authData</h2> 45*19261079SEd Maste<pre id="enrollresultauthdata" style="color: #008; font-family: monospace;"></pre> 46*19261079SEd Maste<h2>SSH pubkey blob</h2> 47*19261079SEd Maste<pre id="enrollresultpkblob" style="color: #008; font-family: monospace;"></pre> 48*19261079SEd Maste<h2>SSH pubkey string</h2> 49*19261079SEd Maste<pre id="enrollresultpk" style="color: #008; font-family: monospace;"></pre> 50*19261079SEd Maste<h2>SSH private key string</h2> 51*19261079SEd Maste<pre id="enrollresultprivkey" style="color: #008; font-family: monospace;"></pre> 52*19261079SEd Maste</span> 53*19261079SEd Maste<span id="assertsection" style="visibility: hidden;"> 54*19261079SEd Maste<h2>Assert</h2> 55*19261079SEd Maste<form id="assertform"> 56*19261079SEd Maste<span id="asserterror" style="color: #800; font-weight: bold;"></span> 57*19261079SEd Maste<table> 58*19261079SEd Maste<tr> 59*19261079SEd Maste<td><b>Data to sign:</b></td> 60*19261079SEd Maste<td><input id="message" type="text" size="20" name="message" value="test" /></td> 61*19261079SEd Maste</tr> 62*19261079SEd Maste<tr> 63*19261079SEd Maste<td><input id="message_sshsig" type="checkbox" checked /> use sshsig format</td> 64*19261079SEd Maste</tr> 65*19261079SEd Maste<tr> 66*19261079SEd Maste<td><b>Signature namespace:</b></td> 67*19261079SEd Maste<td><input id="message_namespace" type="text" size="20" name="namespace" value="test" /></td> 68*19261079SEd Maste</tr> 69*19261079SEd Maste<tr><td></td><td><input type="submit" value="submit" /></td></tr> 70*19261079SEd Maste</table> 71*19261079SEd Maste</form> 72*19261079SEd Maste</span> 73*19261079SEd Maste<span id="assertresult" style="visibility: hidden;"> 74*19261079SEd Maste<h2>clientData</h2> 75*19261079SEd Maste<pre id="assertresultjson" style="color: #008; font-family: monospace;"></pre> 76*19261079SEd Maste<h2>signature raw</h2> 77*19261079SEd Maste<pre id="assertresultsigraw" style="color: #008; font-family: monospace;"></pre> 78*19261079SEd Maste<h2>authenticatorData raw</h2> 79*19261079SEd Maste<pre id="assertresultauthdataraw" style="color: #008; font-family: monospace;"></pre> 80*19261079SEd Maste<h2>authenticatorData</h2> 81*19261079SEd Maste<pre id="assertresultauthdata" style="color: #008; font-family: monospace;"></pre> 82*19261079SEd Maste<h2>signature in SSH format</h2> 83*19261079SEd Maste<pre id="assertresultsshsigraw" style="color: #008; font-family: monospace;"></pre> 84*19261079SEd Maste<h2>signature in SSH format (base64 encoded)</h2> 85*19261079SEd Maste<pre id="assertresultsshsigb64" style="color: #008; font-family: monospace;"></pre> 86*19261079SEd Maste</span> 87*19261079SEd Maste</body> 88*19261079SEd Maste<script> 89*19261079SEd Maste// ------------------------------------------------------------------ 90*19261079SEd Maste// a crappy CBOR decoder - 20200401 djm@openbsd.org 91*19261079SEd Maste 92*19261079SEd Mastevar CBORDecode = function(buffer) { 93*19261079SEd Maste this.buf = buffer 94*19261079SEd Maste this.v = new DataView(buffer) 95*19261079SEd Maste this.offset = 0 96*19261079SEd Maste} 97*19261079SEd Maste 98*19261079SEd MasteCBORDecode.prototype.empty = function() { 99*19261079SEd Maste return this.offset >= this.buf.byteLength 100*19261079SEd Maste} 101*19261079SEd Maste 102*19261079SEd MasteCBORDecode.prototype.getU8 = function() { 103*19261079SEd Maste let r = this.v.getUint8(this.offset) 104*19261079SEd Maste this.offset += 1 105*19261079SEd Maste return r 106*19261079SEd Maste} 107*19261079SEd Maste 108*19261079SEd MasteCBORDecode.prototype.getU16 = function() { 109*19261079SEd Maste let r = this.v.getUint16(this.offset) 110*19261079SEd Maste this.offset += 2 111*19261079SEd Maste return r 112*19261079SEd Maste} 113*19261079SEd Maste 114*19261079SEd MasteCBORDecode.prototype.getU32 = function() { 115*19261079SEd Maste let r = this.v.getUint32(this.offset) 116*19261079SEd Maste this.offset += 4 117*19261079SEd Maste return r 118*19261079SEd Maste} 119*19261079SEd Maste 120*19261079SEd MasteCBORDecode.prototype.getU64 = function() { 121*19261079SEd Maste let r = this.v.getUint64(this.offset) 122*19261079SEd Maste this.offset += 8 123*19261079SEd Maste return r 124*19261079SEd Maste} 125*19261079SEd Maste 126*19261079SEd MasteCBORDecode.prototype.getCBORTypeLen = function() { 127*19261079SEd Maste let tl, t, l 128*19261079SEd Maste tl = this.getU8() 129*19261079SEd Maste t = (tl & 0xe0) >> 5 130*19261079SEd Maste l = tl & 0x1f 131*19261079SEd Maste return [t, this.decodeInteger(l)] 132*19261079SEd Maste} 133*19261079SEd Maste 134*19261079SEd MasteCBORDecode.prototype.decodeInteger = function(len) { 135*19261079SEd Maste switch (len) { 136*19261079SEd Maste case 0x18: return this.getU8() 137*19261079SEd Maste case 0x19: return this.getU16() 138*19261079SEd Maste case 0x20: return this.getU32() 139*19261079SEd Maste case 0x21: return this.getU64() 140*19261079SEd Maste default: 141*19261079SEd Maste if (len <= 23) { 142*19261079SEd Maste return len 143*19261079SEd Maste } 144*19261079SEd Maste throw new Error("Unsupported int type 0x" + len.toString(16)) 145*19261079SEd Maste } 146*19261079SEd Maste} 147*19261079SEd Maste 148*19261079SEd MasteCBORDecode.prototype.decodeNegint = function(len) { 149*19261079SEd Maste let r = -(this.decodeInteger(len) + 1) 150*19261079SEd Maste return r 151*19261079SEd Maste} 152*19261079SEd Maste 153*19261079SEd MasteCBORDecode.prototype.decodeByteString = function(len) { 154*19261079SEd Maste let r = this.buf.slice(this.offset, this.offset + len) 155*19261079SEd Maste this.offset += len 156*19261079SEd Maste return r 157*19261079SEd Maste} 158*19261079SEd Maste 159*19261079SEd MasteCBORDecode.prototype.decodeTextString = function(len) { 160*19261079SEd Maste let u8dec = new TextDecoder('utf-8') 161*19261079SEd Maste r = u8dec.decode(this.decodeByteString(len)) 162*19261079SEd Maste return r 163*19261079SEd Maste} 164*19261079SEd Maste 165*19261079SEd MasteCBORDecode.prototype.decodeArray = function(len, level) { 166*19261079SEd Maste let r = [] 167*19261079SEd Maste for (let i = 0; i < len; i++) { 168*19261079SEd Maste let v = this.decodeInternal(level) 169*19261079SEd Maste r.push(v) 170*19261079SEd Maste // console.log("decodeArray level " + level.toString() + " index " + i.toString() + " value " + JSON.stringify(v)) 171*19261079SEd Maste } 172*19261079SEd Maste return r 173*19261079SEd Maste} 174*19261079SEd Maste 175*19261079SEd MasteCBORDecode.prototype.decodeMap = function(len, level) { 176*19261079SEd Maste let r = {} 177*19261079SEd Maste for (let i = 0; i < len; i++) { 178*19261079SEd Maste let k = this.decodeInternal(level) 179*19261079SEd Maste let v = this.decodeInternal(level) 180*19261079SEd Maste r[k] = v 181*19261079SEd Maste // console.log("decodeMap level " + level.toString() + " key " + k.toString() + " value " + JSON.stringify(v)) 182*19261079SEd Maste // XXX check string keys, duplicates 183*19261079SEd Maste } 184*19261079SEd Maste return r 185*19261079SEd Maste} 186*19261079SEd Maste 187*19261079SEd MasteCBORDecode.prototype.decodePrimitive = function(t) { 188*19261079SEd Maste switch (t) { 189*19261079SEd Maste case 20: return false 190*19261079SEd Maste case 21: return true 191*19261079SEd Maste case 22: return null 192*19261079SEd Maste case 23: return undefined 193*19261079SEd Maste default: 194*19261079SEd Maste throw new Error("Unsupported primitive 0x" + t.toString(2)) 195*19261079SEd Maste } 196*19261079SEd Maste} 197*19261079SEd Maste 198*19261079SEd MasteCBORDecode.prototype.decodeInternal = function(level) { 199*19261079SEd Maste if (level > 256) { 200*19261079SEd Maste throw new Error("CBOR nesting too deep") 201*19261079SEd Maste } 202*19261079SEd Maste let t, l, r 203*19261079SEd Maste [t, l] = this.getCBORTypeLen() 204*19261079SEd Maste // console.log("decode level " + level.toString() + " type " + t.toString() + " len " + l.toString()) 205*19261079SEd Maste switch (t) { 206*19261079SEd Maste case 0: 207*19261079SEd Maste r = this.decodeInteger(l) 208*19261079SEd Maste break 209*19261079SEd Maste case 1: 210*19261079SEd Maste r = this.decodeNegint(l) 211*19261079SEd Maste break 212*19261079SEd Maste case 2: 213*19261079SEd Maste r = this.decodeByteString(l) 214*19261079SEd Maste break 215*19261079SEd Maste case 3: 216*19261079SEd Maste r = this.decodeTextString(l) 217*19261079SEd Maste break 218*19261079SEd Maste case 4: 219*19261079SEd Maste r = this.decodeArray(l, level + 1) 220*19261079SEd Maste break 221*19261079SEd Maste case 5: 222*19261079SEd Maste r = this.decodeMap(l, level + 1) 223*19261079SEd Maste break 224*19261079SEd Maste case 6: 225*19261079SEd Maste console.log("XXX ignored semantic tag " + this.decodeInteger(l).toString()) 226*19261079SEd Maste break; 227*19261079SEd Maste case 7: 228*19261079SEd Maste r = this.decodePrimitive(l) 229*19261079SEd Maste break 230*19261079SEd Maste default: 231*19261079SEd Maste throw new Error("Unsupported type 0x" + t.toString(2) + " len " + l.toString()) 232*19261079SEd Maste } 233*19261079SEd Maste // console.log("decode level " + level.toString() + " value " + JSON.stringify(r)) 234*19261079SEd Maste return r 235*19261079SEd Maste} 236*19261079SEd Maste 237*19261079SEd MasteCBORDecode.prototype.decode = function() { 238*19261079SEd Maste return this.decodeInternal(0) 239*19261079SEd Maste} 240*19261079SEd Maste 241*19261079SEd Maste// ------------------------------------------------------------------ 242*19261079SEd Maste// a crappy SSH message packer - 20200401 djm@openbsd.org 243*19261079SEd Maste 244*19261079SEd Mastevar SSHMSG = function() { 245*19261079SEd Maste this.r = [] 246*19261079SEd Maste} 247*19261079SEd Maste 248*19261079SEd MasteSSHMSG.prototype.length = function() { 249*19261079SEd Maste let len = 0 250*19261079SEd Maste for (buf of this.r) { 251*19261079SEd Maste len += buf.length 252*19261079SEd Maste } 253*19261079SEd Maste return len 254*19261079SEd Maste} 255*19261079SEd Maste 256*19261079SEd MasteSSHMSG.prototype.serialise = function() { 257*19261079SEd Maste let r = new ArrayBuffer(this.length()) 258*19261079SEd Maste let v = new Uint8Array(r) 259*19261079SEd Maste let offset = 0 260*19261079SEd Maste for (buf of this.r) { 261*19261079SEd Maste v.set(buf, offset) 262*19261079SEd Maste offset += buf.length 263*19261079SEd Maste } 264*19261079SEd Maste if (offset != r.byteLength) { 265*19261079SEd Maste throw new Error("djm can't count") 266*19261079SEd Maste } 267*19261079SEd Maste return r 268*19261079SEd Maste} 269*19261079SEd Maste 270*19261079SEd MasteSSHMSG.prototype.serialiseBase64 = function(v) { 271*19261079SEd Maste let b = this.serialise() 272*19261079SEd Maste return btoa(String.fromCharCode(...new Uint8Array(b))); 273*19261079SEd Maste} 274*19261079SEd Maste 275*19261079SEd MasteSSHMSG.prototype.putU8 = function(v) { 276*19261079SEd Maste this.r.push(new Uint8Array([v])) 277*19261079SEd Maste} 278*19261079SEd Maste 279*19261079SEd MasteSSHMSG.prototype.putU32 = function(v) { 280*19261079SEd Maste this.r.push(new Uint8Array([ 281*19261079SEd Maste (v >> 24) & 0xff, 282*19261079SEd Maste (v >> 16) & 0xff, 283*19261079SEd Maste (v >> 8) & 0xff, 284*19261079SEd Maste (v & 0xff) 285*19261079SEd Maste ])) 286*19261079SEd Maste} 287*19261079SEd Maste 288*19261079SEd MasteSSHMSG.prototype.put = function(v) { 289*19261079SEd Maste this.r.push(new Uint8Array(v)) 290*19261079SEd Maste} 291*19261079SEd Maste 292*19261079SEd MasteSSHMSG.prototype.putStringRaw = function(v) { 293*19261079SEd Maste let enc = new TextEncoder(); 294*19261079SEd Maste let venc = enc.encode(v) 295*19261079SEd Maste this.put(venc) 296*19261079SEd Maste} 297*19261079SEd Maste 298*19261079SEd MasteSSHMSG.prototype.putString = function(v) { 299*19261079SEd Maste let enc = new TextEncoder(); 300*19261079SEd Maste let venc = enc.encode(v) 301*19261079SEd Maste this.putU32(venc.length) 302*19261079SEd Maste this.put(venc) 303*19261079SEd Maste} 304*19261079SEd Maste 305*19261079SEd MasteSSHMSG.prototype.putSSHMSG = function(v) { 306*19261079SEd Maste let msg = v.serialise() 307*19261079SEd Maste this.putU32(msg.byteLength) 308*19261079SEd Maste this.put(msg) 309*19261079SEd Maste} 310*19261079SEd Maste 311*19261079SEd MasteSSHMSG.prototype.putBytes = function(v) { 312*19261079SEd Maste this.putU32(v.byteLength) 313*19261079SEd Maste this.put(v) 314*19261079SEd Maste} 315*19261079SEd Maste 316*19261079SEd MasteSSHMSG.prototype.putECPoint = function(x, y) { 317*19261079SEd Maste let x8 = new Uint8Array(x) 318*19261079SEd Maste let y8 = new Uint8Array(y) 319*19261079SEd Maste this.putU32(1 + x8.length + y8.length) 320*19261079SEd Maste this.putU8(0x04) // Uncompressed point format. 321*19261079SEd Maste this.put(x8) 322*19261079SEd Maste this.put(y8) 323*19261079SEd Maste} 324*19261079SEd Maste 325*19261079SEd Maste// ------------------------------------------------------------------ 326*19261079SEd Maste// webauthn to SSH glue - djm@openbsd.org 20200408 327*19261079SEd Maste 328*19261079SEd Mastefunction error(msg, ...args) { 329*19261079SEd Maste document.getElementById("error").innerText = msg 330*19261079SEd Maste console.log(msg) 331*19261079SEd Maste for (const arg of args) { 332*19261079SEd Maste console.dir(arg) 333*19261079SEd Maste } 334*19261079SEd Maste} 335*19261079SEd Mastefunction hexdump(buf) { 336*19261079SEd Maste const hex = Array.from(new Uint8Array(buf)).map( 337*19261079SEd Maste b => b.toString(16).padStart(2, "0")) 338*19261079SEd Maste const fmt = new Array() 339*19261079SEd Maste for (let i = 0; i < hex.length; i++) { 340*19261079SEd Maste if ((i % 16) == 0) { 341*19261079SEd Maste // Prepend length every 16 bytes. 342*19261079SEd Maste fmt.push(i.toString(16).padStart(4, "0")) 343*19261079SEd Maste fmt.push(" ") 344*19261079SEd Maste } 345*19261079SEd Maste fmt.push(hex[i]) 346*19261079SEd Maste fmt.push(" ") 347*19261079SEd Maste if ((i % 16) == 15) { 348*19261079SEd Maste fmt.push("\n") 349*19261079SEd Maste } 350*19261079SEd Maste } 351*19261079SEd Maste return fmt.join("") 352*19261079SEd Maste} 353*19261079SEd Mastefunction enrollform_submit(event) { 354*19261079SEd Maste event.preventDefault(); 355*19261079SEd Maste console.log("submitted") 356*19261079SEd Maste username = event.target.elements.username.value 357*19261079SEd Maste if (username === "") { 358*19261079SEd Maste error("no username specified") 359*19261079SEd Maste return false 360*19261079SEd Maste } 361*19261079SEd Maste enrollStart(username) 362*19261079SEd Maste} 363*19261079SEd Mastefunction enrollStart(username) { 364*19261079SEd Maste let challenge = new Uint8Array(32) 365*19261079SEd Maste window.crypto.getRandomValues(challenge) 366*19261079SEd Maste let userid = new Uint8Array(8) 367*19261079SEd Maste window.crypto.getRandomValues(userid) 368*19261079SEd Maste 369*19261079SEd Maste console.log("challenge:" + btoa(challenge)) 370*19261079SEd Maste console.log("userid:" + btoa(userid)) 371*19261079SEd Maste 372*19261079SEd Maste let pkopts = { 373*19261079SEd Maste challenge: challenge, 374*19261079SEd Maste rp: { 375*19261079SEd Maste name: "mindrot.org", 376*19261079SEd Maste id: "mindrot.org", 377*19261079SEd Maste }, 378*19261079SEd Maste user: { 379*19261079SEd Maste id: userid, 380*19261079SEd Maste name: username, 381*19261079SEd Maste displayName: username, 382*19261079SEd Maste }, 383*19261079SEd Maste authenticatorSelection: { 384*19261079SEd Maste authenticatorAttachment: "cross-platform", 385*19261079SEd Maste userVerification: "discouraged", 386*19261079SEd Maste }, 387*19261079SEd Maste pubKeyCredParams: [{alg: -7, type: "public-key"}], // ES256 388*19261079SEd Maste timeout: 30 * 1000, 389*19261079SEd Maste }; 390*19261079SEd Maste console.dir(pkopts) 391*19261079SEd Maste window.enrollOpts = pkopts 392*19261079SEd Maste let credpromise = navigator.credentials.create({ publicKey: pkopts }); 393*19261079SEd Maste credpromise.then(enrollSuccess, enrollFailure) 394*19261079SEd Maste} 395*19261079SEd Mastefunction enrollFailure(result) { 396*19261079SEd Maste error("Enroll failed", result) 397*19261079SEd Maste} 398*19261079SEd Mastefunction enrollSuccess(result) { 399*19261079SEd Maste console.log("Enroll succeeded") 400*19261079SEd Maste console.dir(result) 401*19261079SEd Maste window.enrollResult = result 402*19261079SEd Maste document.getElementById("enrollresult").style.visibility = "visible" 403*19261079SEd Maste 404*19261079SEd Maste // Show the clientData 405*19261079SEd Maste let u8dec = new TextDecoder('utf-8') 406*19261079SEd Maste clientData = u8dec.decode(result.response.clientDataJSON) 407*19261079SEd Maste document.getElementById("enrollresultjson").innerText = clientData 408*19261079SEd Maste 409*19261079SEd Maste // Show the raw key handle. 410*19261079SEd Maste document.getElementById("keyhandle").innerText = hexdump(result.rawId) 411*19261079SEd Maste 412*19261079SEd Maste // Decode and show the attestationObject 413*19261079SEd Maste document.getElementById("enrollresultraw").innerText = hexdump(result.response.attestationObject) 414*19261079SEd Maste let aod = new CBORDecode(result.response.attestationObject) 415*19261079SEd Maste let attestationObject = aod.decode() 416*19261079SEd Maste console.log("attestationObject") 417*19261079SEd Maste console.dir(attestationObject) 418*19261079SEd Maste document.getElementById("enrollresultattestobj").innerText = JSON.stringify(attestationObject) 419*19261079SEd Maste 420*19261079SEd Maste // Decode and show the authData 421*19261079SEd Maste document.getElementById("enrollresultauthdataraw").innerText = hexdump(attestationObject.authData) 422*19261079SEd Maste let authData = decodeAuthenticatorData(attestationObject.authData, true) 423*19261079SEd Maste console.log("authData") 424*19261079SEd Maste console.dir(authData) 425*19261079SEd Maste window.enrollAuthData = authData 426*19261079SEd Maste document.getElementById("enrollresultauthdata").innerText = JSON.stringify(authData) 427*19261079SEd Maste 428*19261079SEd Maste // Reformat the pubkey as a SSH key for easy verification 429*19261079SEd Maste window.rawKey = reformatPubkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id) 430*19261079SEd Maste console.log("SSH pubkey blob") 431*19261079SEd Maste console.dir(window.rawKey) 432*19261079SEd Maste document.getElementById("enrollresultpkblob").innerText = hexdump(window.rawKey) 433*19261079SEd Maste let pk64 = btoa(String.fromCharCode(...new Uint8Array(window.rawKey))); 434*19261079SEd Maste let pk = "sk-ecdsa-sha2-nistp256@openssh.com " + pk64 435*19261079SEd Maste document.getElementById("enrollresultpk").innerText = pk 436*19261079SEd Maste 437*19261079SEd Maste // Format a private key too. 438*19261079SEd Maste flags = 0x01 // SSH_SK_USER_PRESENCE_REQD 439*19261079SEd Maste window.rawPrivkey = reformatPrivkey(authData.attestedCredentialData.credentialPublicKey, window.enrollOpts.rp.id, result.rawId, flags) 440*19261079SEd Maste let privkeyFileBlob = privkeyFile(window.rawKey, window.rawPrivkey, window.enrollOpts.user.name, window.enrollOpts.rp.id) 441*19261079SEd Maste let privk64 = btoa(String.fromCharCode(...new Uint8Array(privkeyFileBlob))); 442*19261079SEd Maste let privkey = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + wrapString(privk64, 70) + "-----END OPENSSH PRIVATE KEY-----\n" 443*19261079SEd Maste document.getElementById("enrollresultprivkey").innerText = privkey 444*19261079SEd Maste 445*19261079SEd Maste // Success: show the assertion form. 446*19261079SEd Maste document.getElementById("assertsection").style.visibility = "visible" 447*19261079SEd Maste} 448*19261079SEd Maste 449*19261079SEd Mastefunction decodeAuthenticatorData(authData, expectCred) { 450*19261079SEd Maste let r = new Object() 451*19261079SEd Maste let v = new DataView(authData) 452*19261079SEd Maste 453*19261079SEd Maste r.rpIdHash = authData.slice(0, 32) 454*19261079SEd Maste r.flags = v.getUint8(32) 455*19261079SEd Maste r.signCount = v.getUint32(33) 456*19261079SEd Maste 457*19261079SEd Maste // Decode attestedCredentialData if present. 458*19261079SEd Maste let offset = 37 459*19261079SEd Maste let acd = new Object() 460*19261079SEd Maste if (expectCred) { 461*19261079SEd Maste acd.aaguid = authData.slice(offset, offset+16) 462*19261079SEd Maste offset += 16 463*19261079SEd Maste let credentialIdLength = v.getUint16(offset) 464*19261079SEd Maste offset += 2 465*19261079SEd Maste acd.credentialIdLength = credentialIdLength 466*19261079SEd Maste acd.credentialId = authData.slice(offset, offset+credentialIdLength) 467*19261079SEd Maste offset += credentialIdLength 468*19261079SEd Maste r.attestedCredentialData = acd 469*19261079SEd Maste } 470*19261079SEd Maste console.log("XXXXX " + offset.toString()) 471*19261079SEd Maste let pubkeyrest = authData.slice(offset, authData.byteLength) 472*19261079SEd Maste let pkdecode = new CBORDecode(pubkeyrest) 473*19261079SEd Maste if (expectCred) { 474*19261079SEd Maste // XXX unsafe: doesn't mandate COSE canonical format. 475*19261079SEd Maste acd.credentialPublicKey = pkdecode.decode() 476*19261079SEd Maste } 477*19261079SEd Maste if (!pkdecode.empty()) { 478*19261079SEd Maste // Decode extensions if present. 479*19261079SEd Maste r.extensions = pkdecode.decode() 480*19261079SEd Maste } 481*19261079SEd Maste return r 482*19261079SEd Maste} 483*19261079SEd Maste 484*19261079SEd Mastefunction wrapString(s, l) { 485*19261079SEd Maste ret = "" 486*19261079SEd Maste for (i = 0; i < s.length; i += l) { 487*19261079SEd Maste ret += s.slice(i, i + l) + "\n" 488*19261079SEd Maste } 489*19261079SEd Maste return ret 490*19261079SEd Maste} 491*19261079SEd Maste 492*19261079SEd Mastefunction checkPubkey(pk) { 493*19261079SEd Maste // pk is in COSE format. We only care about a tiny subset. 494*19261079SEd Maste if (pk[1] != 2) { 495*19261079SEd Maste console.dir(pk) 496*19261079SEd Maste throw new Error("pubkey is not EC") 497*19261079SEd Maste } 498*19261079SEd Maste if (pk[-1] != 1) { 499*19261079SEd Maste throw new Error("pubkey is not in P256") 500*19261079SEd Maste } 501*19261079SEd Maste if (pk[3] != -7) { 502*19261079SEd Maste throw new Error("pubkey is not ES256") 503*19261079SEd Maste } 504*19261079SEd Maste if (pk[-2].byteLength != 32 || pk[-3].byteLength != 32) { 505*19261079SEd Maste throw new Error("pubkey EC coords have bad length") 506*19261079SEd Maste } 507*19261079SEd Maste} 508*19261079SEd Maste 509*19261079SEd Mastefunction reformatPubkey(pk, rpid) { 510*19261079SEd Maste checkPubkey(pk) 511*19261079SEd Maste let msg = new SSHMSG() 512*19261079SEd Maste msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type 513*19261079SEd Maste msg.putString("nistp256") // Key curve 514*19261079SEd Maste msg.putECPoint(pk[-2], pk[-3]) // EC key 515*19261079SEd Maste msg.putString(rpid) // RP ID 516*19261079SEd Maste return msg.serialise() 517*19261079SEd Maste} 518*19261079SEd Maste 519*19261079SEd Mastefunction reformatPrivkey(pk, rpid, kh, flags) { 520*19261079SEd Maste checkPubkey(pk) 521*19261079SEd Maste let msg = new SSHMSG() 522*19261079SEd Maste msg.putString("sk-ecdsa-sha2-nistp256@openssh.com") // Key type 523*19261079SEd Maste msg.putString("nistp256") // Key curve 524*19261079SEd Maste msg.putECPoint(pk[-2], pk[-3]) // EC key 525*19261079SEd Maste msg.putString(rpid) // RP ID 526*19261079SEd Maste msg.putU8(flags) // flags 527*19261079SEd Maste msg.putBytes(kh) // handle 528*19261079SEd Maste msg.putString("") // reserved 529*19261079SEd Maste return msg.serialise() 530*19261079SEd Maste} 531*19261079SEd Maste 532*19261079SEd Mastefunction privkeyFile(pub, priv, user, rp) { 533*19261079SEd Maste let innerMsg = new SSHMSG() 534*19261079SEd Maste innerMsg.putU32(0xdeadbeef) // check byte 535*19261079SEd Maste innerMsg.putU32(0xdeadbeef) // check byte 536*19261079SEd Maste innerMsg.put(priv) // privkey 537*19261079SEd Maste innerMsg.putString("webauthn.html " + user + "@" + rp) // comment 538*19261079SEd Maste // Pad to cipher blocksize (8). 539*19261079SEd Maste p = 1 540*19261079SEd Maste while (innerMsg.length() % 8 != 0) { 541*19261079SEd Maste innerMsg.putU8(p++) 542*19261079SEd Maste } 543*19261079SEd Maste let msg = new SSHMSG() 544*19261079SEd Maste msg.putStringRaw("openssh-key-v1") // Magic 545*19261079SEd Maste msg.putU8(0) // \0 terminate 546*19261079SEd Maste msg.putString("none") // cipher 547*19261079SEd Maste msg.putString("none") // KDF 548*19261079SEd Maste msg.putString("") // KDF options 549*19261079SEd Maste msg.putU32(1) // nkeys 550*19261079SEd Maste msg.putBytes(pub) // pubkey 551*19261079SEd Maste msg.putSSHMSG(innerMsg) // inner 552*19261079SEd Maste //msg.put(innerMsg.serialise()) // inner 553*19261079SEd Maste return msg.serialise() 554*19261079SEd Maste} 555*19261079SEd Maste 556*19261079SEd Masteasync function assertform_submit(event) { 557*19261079SEd Maste event.preventDefault(); 558*19261079SEd Maste console.log("submitted") 559*19261079SEd Maste message = event.target.elements.message.value 560*19261079SEd Maste if (message === "") { 561*19261079SEd Maste error("no message specified") 562*19261079SEd Maste return false 563*19261079SEd Maste } 564*19261079SEd Maste let enc = new TextEncoder() 565*19261079SEd Maste let encmsg = enc.encode(message) 566*19261079SEd Maste window.assertSignRaw = !event.target.elements.message_sshsig.checked 567*19261079SEd Maste console.log("using sshsig ", !window.assertSignRaw) 568*19261079SEd Maste if (window.assertSignRaw) { 569*19261079SEd Maste assertStart(encmsg) 570*19261079SEd Maste return 571*19261079SEd Maste } 572*19261079SEd Maste // Format a sshsig-style message. 573*19261079SEd Maste window.sigHashAlg = "sha512" 574*19261079SEd Maste let msghash = await crypto.subtle.digest("SHA-512", encmsg); 575*19261079SEd Maste console.log("raw message hash") 576*19261079SEd Maste console.dir(msghash) 577*19261079SEd Maste window.sigNamespace = event.target.elements.message_namespace.value 578*19261079SEd Maste let sigbuf = new SSHMSG() 579*19261079SEd Maste sigbuf.put(enc.encode("SSHSIG")) 580*19261079SEd Maste sigbuf.putString(window.sigNamespace) 581*19261079SEd Maste sigbuf.putU32(0) // Reserved string 582*19261079SEd Maste sigbuf.putString(window.sigHashAlg) 583*19261079SEd Maste sigbuf.putBytes(msghash) 584*19261079SEd Maste let msg = sigbuf.serialise() 585*19261079SEd Maste console.log("sigbuf") 586*19261079SEd Maste console.dir(msg) 587*19261079SEd Maste assertStart(msg) 588*19261079SEd Maste} 589*19261079SEd Maste 590*19261079SEd Mastefunction assertStart(message) { 591*19261079SEd Maste let assertReqOpts = { 592*19261079SEd Maste challenge: message, 593*19261079SEd Maste rpId: "mindrot.org", 594*19261079SEd Maste allowCredentials: [{ 595*19261079SEd Maste type: 'public-key', 596*19261079SEd Maste id: window.enrollResult.rawId, 597*19261079SEd Maste }], 598*19261079SEd Maste userVerification: "discouraged", 599*19261079SEd Maste timeout: (30 * 1000), 600*19261079SEd Maste } 601*19261079SEd Maste console.log("assertReqOpts") 602*19261079SEd Maste console.dir(assertReqOpts) 603*19261079SEd Maste window.assertReqOpts = assertReqOpts 604*19261079SEd Maste let assertpromise = navigator.credentials.get({ 605*19261079SEd Maste publicKey: assertReqOpts 606*19261079SEd Maste }); 607*19261079SEd Maste assertpromise.then(assertSuccess, assertFailure) 608*19261079SEd Maste} 609*19261079SEd Mastefunction assertFailure(result) { 610*19261079SEd Maste error("Assertion failed", result) 611*19261079SEd Maste} 612*19261079SEd Mastefunction linewrap(s) { 613*19261079SEd Maste const linelen = 70 614*19261079SEd Maste let ret = "" 615*19261079SEd Maste for (let i = 0; i < s.length; i += linelen) { 616*19261079SEd Maste end = i + linelen 617*19261079SEd Maste if (end > s.length) { 618*19261079SEd Maste end = s.length 619*19261079SEd Maste } 620*19261079SEd Maste if (i > 0) { 621*19261079SEd Maste ret += "\n" 622*19261079SEd Maste } 623*19261079SEd Maste ret += s.slice(i, end) 624*19261079SEd Maste } 625*19261079SEd Maste return ret + "\n" 626*19261079SEd Maste} 627*19261079SEd Mastefunction assertSuccess(result) { 628*19261079SEd Maste console.log("Assertion succeeded") 629*19261079SEd Maste console.dir(result) 630*19261079SEd Maste window.assertResult = result 631*19261079SEd Maste document.getElementById("assertresult").style.visibility = "visible" 632*19261079SEd Maste 633*19261079SEd Maste // show the clientData. 634*19261079SEd Maste let u8dec = new TextDecoder('utf-8') 635*19261079SEd Maste clientData = u8dec.decode(result.response.clientDataJSON) 636*19261079SEd Maste document.getElementById("assertresultjson").innerText = clientData 637*19261079SEd Maste 638*19261079SEd Maste // show the signature. 639*19261079SEd Maste document.getElementById("assertresultsigraw").innerText = hexdump(result.response.signature) 640*19261079SEd Maste 641*19261079SEd Maste // decode and show the authData. 642*19261079SEd Maste document.getElementById("assertresultauthdataraw").innerText = hexdump(result.response.authenticatorData) 643*19261079SEd Maste authData = decodeAuthenticatorData(result.response.authenticatorData, false) 644*19261079SEd Maste document.getElementById("assertresultauthdata").innerText = JSON.stringify(authData) 645*19261079SEd Maste 646*19261079SEd Maste // Parse and reformat the signature to an SSH style signature. 647*19261079SEd Maste let sshsig = reformatSignature(result.response.signature, clientData, authData) 648*19261079SEd Maste document.getElementById("assertresultsshsigraw").innerText = hexdump(sshsig) 649*19261079SEd Maste let sig64 = btoa(String.fromCharCode(...new Uint8Array(sshsig))); 650*19261079SEd Maste if (window.assertSignRaw) { 651*19261079SEd Maste document.getElementById("assertresultsshsigb64").innerText = sig64 652*19261079SEd Maste } else { 653*19261079SEd Maste document.getElementById("assertresultsshsigb64").innerText = 654*19261079SEd Maste "-----BEGIN SSH SIGNATURE-----\n" + linewrap(sig64) + 655*19261079SEd Maste "-----END SSH SIGNATURE-----\n"; 656*19261079SEd Maste } 657*19261079SEd Maste} 658*19261079SEd Maste 659*19261079SEd Mastefunction reformatSignature(sig, clientData, authData) { 660*19261079SEd Maste if (sig.byteLength < 2) { 661*19261079SEd Maste throw new Error("signature is too short") 662*19261079SEd Maste } 663*19261079SEd Maste let offset = 0 664*19261079SEd Maste let v = new DataView(sig) 665*19261079SEd Maste // Expect an ASN.1 SEQUENCE that exactly spans the signature. 666*19261079SEd Maste if (v.getUint8(offset) != 0x30) { 667*19261079SEd Maste throw new Error("signature not an ASN.1 sequence") 668*19261079SEd Maste } 669*19261079SEd Maste offset++ 670*19261079SEd Maste let seqlen = v.getUint8(offset) 671*19261079SEd Maste offset++ 672*19261079SEd Maste if ((seqlen & 0x80) != 0 || seqlen != sig.byteLength - offset) { 673*19261079SEd Maste throw new Error("signature has unexpected length " + seqlen.toString() + " vs expected " + (sig.byteLength - offset).toString()) 674*19261079SEd Maste } 675*19261079SEd Maste 676*19261079SEd Maste // Parse 'r' INTEGER value. 677*19261079SEd Maste if (v.getUint8(offset) != 0x02) { 678*19261079SEd Maste throw new Error("signature r not an ASN.1 integer") 679*19261079SEd Maste } 680*19261079SEd Maste offset++ 681*19261079SEd Maste let rlen = v.getUint8(offset) 682*19261079SEd Maste offset++ 683*19261079SEd Maste if ((rlen & 0x80) != 0 || rlen > sig.byteLength - offset) { 684*19261079SEd Maste throw new Error("signature r has unexpected length " + rlen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) 685*19261079SEd Maste } 686*19261079SEd Maste let r = sig.slice(offset, offset + rlen) 687*19261079SEd Maste offset += rlen 688*19261079SEd Maste console.log("sig_r") 689*19261079SEd Maste console.dir(r) 690*19261079SEd Maste 691*19261079SEd Maste // Parse 's' INTEGER value. 692*19261079SEd Maste if (v.getUint8(offset) != 0x02) { 693*19261079SEd Maste throw new Error("signature r not an ASN.1 integer") 694*19261079SEd Maste } 695*19261079SEd Maste offset++ 696*19261079SEd Maste let slen = v.getUint8(offset) 697*19261079SEd Maste offset++ 698*19261079SEd Maste if ((slen & 0x80) != 0 || slen > sig.byteLength - offset) { 699*19261079SEd Maste throw new Error("signature s has unexpected length " + slen.toString() + " vs buffer " + (sig.byteLength - offset).toString()) 700*19261079SEd Maste } 701*19261079SEd Maste let s = sig.slice(offset, offset + slen) 702*19261079SEd Maste console.log("sig_s") 703*19261079SEd Maste console.dir(s) 704*19261079SEd Maste offset += slen 705*19261079SEd Maste 706*19261079SEd Maste if (offset != sig.byteLength) { 707*19261079SEd Maste throw new Error("unexpected final offset during signature parsing " + offset.toString() + " expected " + sig.byteLength.toString()) 708*19261079SEd Maste } 709*19261079SEd Maste 710*19261079SEd Maste // Reformat as an SSH signature. 711*19261079SEd Maste let clientDataParsed = JSON.parse(clientData) 712*19261079SEd Maste let innersig = new SSHMSG() 713*19261079SEd Maste innersig.putBytes(r) 714*19261079SEd Maste innersig.putBytes(s) 715*19261079SEd Maste 716*19261079SEd Maste let rawsshsig = new SSHMSG() 717*19261079SEd Maste rawsshsig.putString("webauthn-sk-ecdsa-sha2-nistp256@openssh.com") 718*19261079SEd Maste rawsshsig.putSSHMSG(innersig) 719*19261079SEd Maste rawsshsig.putU8(authData.flags) 720*19261079SEd Maste rawsshsig.putU32(authData.signCount) 721*19261079SEd Maste rawsshsig.putString(clientDataParsed.origin) 722*19261079SEd Maste rawsshsig.putString(clientData) 723*19261079SEd Maste if (authData.extensions == undefined) { 724*19261079SEd Maste rawsshsig.putU32(0) 725*19261079SEd Maste } else { 726*19261079SEd Maste rawsshsig.putBytes(authData.extensions) 727*19261079SEd Maste } 728*19261079SEd Maste 729*19261079SEd Maste if (window.assertSignRaw) { 730*19261079SEd Maste return rawsshsig.serialise() 731*19261079SEd Maste } 732*19261079SEd Maste // Format as SSHSIG. 733*19261079SEd Maste let enc = new TextEncoder() 734*19261079SEd Maste let sshsig = new SSHMSG() 735*19261079SEd Maste sshsig.put(enc.encode("SSHSIG")) 736*19261079SEd Maste sshsig.putU32(0x01) // Signature version. 737*19261079SEd Maste sshsig.putBytes(window.rawKey) 738*19261079SEd Maste sshsig.putString(window.sigNamespace) 739*19261079SEd Maste sshsig.putU32(0) // Reserved string 740*19261079SEd Maste sshsig.putString(window.sigHashAlg) 741*19261079SEd Maste sshsig.putBytes(rawsshsig.serialise()) 742*19261079SEd Maste return sshsig.serialise() 743*19261079SEd Maste} 744*19261079SEd Maste 745*19261079SEd Mastefunction toggleNamespaceVisibility() { 746*19261079SEd Maste const assertsigtype = document.getElementById('message_sshsig'); 747*19261079SEd Maste const assertsignamespace = document.getElementById('message_namespace'); 748*19261079SEd Maste assertsignamespace.disabled = !assertsigtype.checked; 749*19261079SEd Maste} 750*19261079SEd Maste 751*19261079SEd Mastefunction init() { 752*19261079SEd Maste if (document.location.protocol != "https:") { 753*19261079SEd Maste error("This page must be loaded via https") 754*19261079SEd Maste const assertsubmit = document.getElementById('assertsubmit') 755*19261079SEd Maste assertsubmit.disabled = true 756*19261079SEd Maste } 757*19261079SEd Maste const enrollform = document.getElementById('enrollform'); 758*19261079SEd Maste enrollform.addEventListener('submit', enrollform_submit); 759*19261079SEd Maste const assertform = document.getElementById('assertform'); 760*19261079SEd Maste assertform.addEventListener('submit', assertform_submit); 761*19261079SEd Maste const assertsigtype = document.getElementById('message_sshsig'); 762*19261079SEd Maste assertsigtype.onclick = toggleNamespaceVisibility; 763*19261079SEd Maste} 764*19261079SEd Maste</script> 765*19261079SEd Maste 766*19261079SEd Maste</html> 767