xref: /freebsd/crypto/openssh/regress/unittests/sshsig/webauthn.html (revision 1323ec571215a77ddd21294f0871979d5ad6b992)
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