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