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