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