Browse Source

progress on bacme

AJ ONeal 7 months ago
parent
commit
4fd5fd8bd9
5 changed files with 491 additions and 2 deletions
  1. 1
    0
      .gitignore
  2. 3
    0
      index.html
  3. 8
    0
      install.sh
  4. 1
    2
      js/app.js
  5. 478
    0
      js/bacme.js

+ 1
- 0
.gitignore View File

@@ -1 +1,2 @@
1 1
 js/pkijs.org
2
+js/browser-csr

+ 3
- 0
index.html View File

@@ -14,6 +14,9 @@
14 14
     <script src="./js/pkijs.org/v1.3.33/asn1.js"></script>
15 15
     <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script>
16 16
     <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script>
17
+    <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script>
18
+
19
+    <script src="./js/bacme.js"></script>
17 20
     <script src="./js/app.js"></script>
18 21
   </body>
19 22
 </html>

+ 8
- 0
install.sh View File

@@ -1,6 +1,14 @@
1
+#!/bin/bash
2
+
1 3
 mkdir -p js/pkijs.org/v1.3.33/
2 4
 pushd js/pkijs.org/v1.3.33/
3 5
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js
4 6
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js
5 7
   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js
6 8
   wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js
9
+popd
10
+
11
+mkdir -p js/browser-csr/v1.0.0-alpha/
12
+pushd js/browser-csr/v1.0.0-alpha/
13
+  wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/c513a862a4e016794da800f0c2eec858b80837ab/csr.js
14
+popd

+ 1
- 2
js/app.js View File

@@ -1,7 +1,6 @@
1 1
 (function () {
2 2
 'use strict';
3 3
 
4
-  console.log("Hello, World!");
5
-
4
+  //window.document.querySelector('.js-acme-directory-url').value = 'https://acme-v02.api.letsencrypt.org/directory';
6 5
   window.document.querySelector('.js-acme-directory-url').value = 'https://acme-staging-v02.api.letsencrypt.org/directory';
7 6
 }());

+ 478
- 0
js/bacme.js View File

@@ -0,0 +1,478 @@
1
+(function (exports) {
2
+'use strict';
3
+
4
+var BACME = exports.BACME = {};
5
+var webFetch = exports.fetch;
6
+var webCrypto = exports.crypto;
7
+
8
+var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
9
+var directory;
10
+
11
+var nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce';
12
+var nonce;
13
+
14
+var accountKeypair;
15
+var accountJwk;
16
+
17
+var accountUrl = directory.newAccount;
18
+var signedAccount;
19
+
20
+BACME.challengePrefixes = {
21
+  'http-01': '/.well-known/acme-challenge'
22
+, 'dns-01': '_acme-challenge'
23
+};
24
+
25
+BACME._logHeaders = function (resp) {
26
+	console.log('Headers:');
27
+	Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
28
+};
29
+
30
+BACME._logBody = function (body) {
31
+	console.log('Body:');
32
+	console.log(JSON.stringify(body, null, 2));
33
+	console.log('');
34
+};
35
+
36
+BACME.directory = function (url) {
37
+	return webFetch(directoryUrl, { mode: 'cors' }).then(function (resp) {
38
+		BACME._logHeaders(resp);
39
+		return resp.json().then(function (body) {
40
+			directory = body;
41
+      BACME._logBody(body);
42
+      return body;
43
+		});
44
+	});
45
+};
46
+
47
+BACME.nonce = function () {
48
+	return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) {
49
+    BACME._logHeaders(resp);
50
+		nonce = resp.headers.get('replay-nonce');
51
+		console.log('Nonce:', nonce);
52
+		// resp.body is empty
53
+		return resp.headers.get('replay-nonce');
54
+	});
55
+};
56
+
57
+BACME.accounts = {};
58
+BACME.accounts.generateKeypair = function () {
59
+	// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
60
+	var extractable = true;
61
+	return webCrypto.subtle.generateKey(
62
+		{ name: "ECDSA", namedCurve: "P-256" }
63
+	, extractable
64
+	, [ 'sign', 'verify' ]
65
+	).then(function (result) {
66
+		accountKeypair = result;
67
+
68
+		return webCrypto.subtle.exportKey(
69
+			"jwk"
70
+		, result.privateKey
71
+		).then(function (jwk) {
72
+
73
+			accountJwk = jwk;
74
+			console.log('private jwk:');
75
+			console.log(JSON.stringify(jwk, null, 2));
76
+
77
+			return webCrypto.subtle.exportKey(
78
+				"pkcs8"
79
+			, result.privateKey
80
+			).then(function (keydata) {
81
+				console.log('pkcs8:');
82
+				console.log(Array.from(new Uint8Array(keydata)));
83
+
84
+        return accountKeypair;
85
+			});
86
+		})
87
+	});
88
+};
89
+
90
+// json to url-safe base64
91
+BACME._jsto64 = function (json) {
92
+	return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
93
+};
94
+
95
+var textEncoder = new TextEncoder();
96
+
97
+// email = john.doe@gmail.com
98
+BACME.accounts.sign = function (email) {
99
+	var payload64 = BACME._jsto64(
100
+		{ termsOfServiceAgreed: true
101
+		, onlyReturnExisting: false
102
+		, contact: [ 'mailto:' + email ]
103
+		}
104
+	);
105
+
106
+	var protected64 = BACME._jsto64(
107
+		{ nonce: nonce
108
+		, url: accountUrl
109
+		, alg: 'ES256'
110
+		, jwk: {
111
+				kty: accountJwk.kty
112
+			, crv: accountJwk.crv
113
+			, x: accountJwk.x
114
+			, y: accountJwk.y
115
+			}
116
+		}
117
+	);
118
+
119
+	// Note: this function hashes before signing so send data, not the hash
120
+	return window.crypto.subtle.sign(
121
+		{ name: "ECDSA", hash: { name: "SHA-256" } }
122
+	, accountKeypair.privateKey
123
+	, textEncoder.encode(protected64 + '.' + payload64)
124
+	).then(function (signature) {
125
+
126
+		// convert buffer to urlsafe base64
127
+		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
128
+			return String.fromCharCode(ch);
129
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
130
+
131
+		console.log('URL-safe Base64 Signature:');
132
+		console.log(sig64);
133
+
134
+		signedAccount = {
135
+			protected: protected64
136
+		, payload: payload64
137
+		, signature: sig64
138
+		};
139
+		console.log('Signed Base64 Account:');
140
+		console.log(JSON.stringify(signedAccount, null, 2));
141
+	});
142
+};
143
+
144
+var account;
145
+var accountId;
146
+
147
+BACME.accounts.set = function () {
148
+	nonce = null;
149
+	return window.fetch(accountUrl, {
150
+		mode: 'cors'
151
+	, method: 'POST'
152
+	, headers: { 'Content-Type': 'application/jose+json' }
153
+	, body: JSON.stringify(signedAccount)
154
+	}).then(function (resp) {
155
+		BACME._logHeaders(resp);
156
+		nonce = resp.headers.get('replay-nonce');
157
+		accountId = resp.headers.get('location');
158
+		console.log('Next nonce:', nonce);
159
+		console.log('Location/kid:', accountId);
160
+
161
+		if (!resp.headers.get('content-type')) {
162
+		 console.log('Body: <none>');
163
+		 return;
164
+		}
165
+
166
+		return resp.json().then(function (result) {
167
+      BACME._logBody(result);
168
+		});
169
+	});
170
+};
171
+
172
+var orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order";
173
+var signedOrder;
174
+
175
+BACME.orders = {};
176
+
177
+// identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
178
+BACME.orders.sign = function (identifiers) {
179
+	var payload64 = jsto64({ identifiers: identifiers });
180
+
181
+	var protected64 = jsto64(
182
+		{ nonce: nonce, alg: 'ES256', url: orderUrl, kid: accountId }
183
+	);
184
+
185
+	return window.crypto.subtle.sign(
186
+		{ name: "ECDSA", hash: { name: "SHA-256" } }
187
+	, accountKeypair.privateKey
188
+	, textEncoder.encode(protected64 + '.' + payload64)
189
+	).then(function (signature) {
190
+
191
+		// convert buffer to urlsafe base64
192
+		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
193
+			return String.fromCharCode(ch);
194
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
195
+
196
+		console.log('URL-safe Base64 Signature:');
197
+		console.log(sig64);
198
+
199
+		signedOrder = {
200
+			protected: protected64
201
+		, payload: payload64
202
+		, signature: sig64
203
+		};
204
+		console.log('Signed Base64 Order:');
205
+		console.log(JSON.stringify(signedAccount, null, 2));
206
+
207
+    return signedOrder;
208
+	});
209
+};
210
+
211
+var order;
212
+var currentOrderUrl;
213
+var authorizationUrls;
214
+var finalizeUrl;
215
+
216
+BACME.orders.create = function () {
217
+	nonce = null;
218
+	return window.fetch(orderUrl, {
219
+		mode: 'cors'
220
+	, method: 'POST'
221
+	, headers: { 'Content-Type': 'application/jose+json' }
222
+	, body: JSON.stringify(signedOrder)
223
+	}).then(function (resp) {
224
+		console.log('Headers:');
225
+		Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
226
+		currentOrderUrl = resp.headers.get('location');
227
+		nonce = resp.headers.get('replay-nonce');
228
+		console.log('Next nonce:', nonce);
229
+
230
+		return resp.json().then(function (result) {
231
+			authorizationUrls = result.authorizations;
232
+			finalizeUrl = result.finalize;
233
+			console.log('Body:');
234
+			console.log(JSON.stringify(result, null, 2));
235
+
236
+      return result;
237
+		});
238
+	});
239
+};
240
+
241
+BACME.challenges = {};
242
+BACME.challenges.view = function () {
243
+	var authzUrl = authorizationUrls.pop();
244
+	var token;
245
+	var challengeDomain;
246
+	var challengeUrl;
247
+
248
+	return window.fetch(authzUrl, {
249
+		mode: 'cors'
250
+	}).then(function (resp) {
251
+    BACME._logHeaders(resp);
252
+
253
+		return resp.json().then(function (result) {
254
+			// Note: select the challenge you wish to use
255
+			var challenge = result.challenges.slice(0).pop();
256
+			token = challenge.token;
257
+			challengeUrl = challenge.url;
258
+			challengeDomain = result.identifier.value;
259
+
260
+      BACME._logBody(result);
261
+
262
+      return { token: challenge.token, url: challenge.url, domain: result.identifier.value, challenges: result.challenges };
263
+		});
264
+	});
265
+};
266
+
267
+var thumbprint;
268
+var keyAuth;
269
+var httpPath;
270
+var dnsAuth;
271
+var dnsRecord;
272
+
273
+BACME.thumbprint = function () {
274
+	// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
275
+
276
+	var accountPublicStr = '{' + ['crv', 'kty', 'x', 'y'].map(function (key) {
277
+		return '"' + key + '":"' + accountJwk[key] + '"';
278
+	}).join(',') + '}';
279
+
280
+	return window.crypto.subtle.digest(
281
+		{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
282
+	, textEncoder.encode(accountPublicStr)
283
+	).then(function(hash){
284
+		thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
285
+			return String.fromCharCode(ch);
286
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
287
+
288
+		console.log('Thumbprint:');
289
+		console.log(thumbprint);
290
+
291
+    return thumbprint;
292
+	});
293
+};
294
+
295
+BACME.challenges['http-01'] = function () {
296
+	// The contents of the key authorization file
297
+	keyAuth = token + '.' + thumbprint;
298
+
299
+	// Where the key authorization file goes
300
+	httpPath = 'http://' + challengeDomain + '/.well-known/acme-challenge/' + token;
301
+
302
+  console.log("echo '" + keyAuth + "' > '" + httpPath + "'");
303
+
304
+  return {
305
+    path: httpPath
306
+  , value: keyAuth
307
+  };
308
+});
309
+BACME.challenges['dns-01'] = function () {
310
+	return window.crypto.subtle.digest(
311
+		{ name: "SHA-256", }
312
+	, textEncoder.encode(keyAuth)
313
+	).then(function(hash){
314
+		dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) {
315
+			return String.fromCharCode(ch);
316
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
317
+
318
+		dnsRecord = '_acme-challenge.' + challengeDomain;
319
+
320
+		console.log('DNS TXT Auth:');
321
+		// The name of the record
322
+		console.log(dnsRecord);
323
+		// The TXT record value
324
+		console.log(dnsAuth);
325
+
326
+    return {
327
+      type: 'TXT'
328
+    , host: dnsRecord
329
+    , answer: dnsAuth;
330
+    };
331
+	});
332
+};
333
+
334
+var challengePollUrl;
335
+
336
+BACME.challenges.accept = function () {
337
+  var payload64 = jsto64(
338
+		{}
339
+	);
340
+
341
+	var protected64 = jsto64(
342
+		{ nonce: nonce, alg: 'ES256', url: challengeUrl, kid: accountId }
343
+	);
344
+
345
+	nonce = null;
346
+	return window.crypto.subtle.sign(
347
+		{ name: "ECDSA", hash: { name: "SHA-256" } }
348
+	, accountKeypair.privateKey
349
+	, textEncoder.encode(protected64 + '.' + payload64)
350
+	).then(function (signature) {
351
+
352
+		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
353
+			return String.fromCharCode(ch);
354
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
355
+
356
+		var body = {
357
+			protected: protected64
358
+		, payload: payload64
359
+		, signature: sig64
360
+		};
361
+
362
+		return window.fetch(
363
+			challengeUrl
364
+		, { mode: 'cors'
365
+			, method: 'POST'
366
+			, headers: { 'Content-Type': 'application/jose+json' }
367
+			, body: JSON.stringify(body)
368
+			}
369
+		).then(function (resp) {
370
+			console.log('Headers:');
371
+			Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); });
372
+			nonce = resp.headers.get('replay-nonce');
373
+
374
+			return resp.json().then(function (reply) {
375
+				challengePollUrl = reply.url;
376
+
377
+				console.log('Challenge ACK:');
378
+				console.log(JSON.stringify(reply));
379
+			});
380
+		});
381
+	});
382
+};
383
+
384
+BACME.challenges.check = function () {
385
+	return window.fetch(challengePollUrl, { mode: 'cors' }).then(function (resp) {
386
+    BACME._logHeaders(resp);
387
+		nonce = resp.headers.get('replay-nonce');
388
+
389
+		return resp.json().then(function (reply) {
390
+			challengePollUrl = reply.url;
391
+
392
+      BACME._logBody(reply);
393
+
394
+			return reply;
395
+		});
396
+	});
397
+};
398
+
399
+var domainKeypair;
400
+var domainJwk;
401
+
402
+BACME.domains = {};
403
+// TODO factor out from BACME.accounts.generateKeypair
404
+BACME.domains.generateKeypair = function () {
405
+	var extractable = true;
406
+	return window.crypto.subtle.generateKey(
407
+		{ name: "ECDSA", namedCurve: "P-256" }
408
+	, extractable
409
+	, [ 'sign', 'verify' ]
410
+	).then(function (result) {
411
+		domainKeypair = result;
412
+
413
+		return window.crypto.subtle.exportKey(
414
+			"jwk"
415
+		, result.privateKey
416
+		).then(function (jwk) {
417
+
418
+			domainJwk = jwk;
419
+			console.log('private jwk:');
420
+			console.log(JSON.stringify(jwk, null, 2));
421
+
422
+      return domainKeypair;
423
+		})
424
+	});
425
+};
426
+
427
+BACME.order.generateCsr = function (keypair, domains) {
428
+  return Promise.resolve(CSR.generate(keypair, domains));
429
+};
430
+
431
+var certificateUrl;
432
+
433
+BACME.order.finalize = function () {
434
+	var payload64 = jsto64(
435
+		{ csr: csr }
436
+	);
437
+
438
+	var protected64 = jsto64(
439
+		{ nonce: nonce, alg: 'ES256', url: finalizeUrl, kid: accountId }
440
+	);
441
+
442
+	nonce = null;
443
+	return window.crypto.subtle.sign(
444
+		{ name: "ECDSA", hash: { name: "SHA-256" } }
445
+	, accountKeypair.privateKey
446
+	, textEncoder.encode(protected64 + '.' + payload64)
447
+	).then(function (signature) {
448
+
449
+		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) {
450
+			return String.fromCharCode(ch);
451
+		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '');
452
+
453
+		var body = {
454
+			protected: protected64
455
+		, payload: payload64
456
+		, signature: sig64
457
+		};
458
+
459
+		return window.fetch(
460
+			finalizeUrl
461
+		, { mode: 'cors'
462
+			, method: 'POST'
463
+			, headers: { 'Content-Type': 'application/jose+json' }
464
+			, body: JSON.stringify(body)
465
+			}
466
+		).then(function (resp) {
467
+      BACME._logHeaders(resp);
468
+			nonce = resp.headers.get('replay-nonce');
469
+
470
+			return resp.json().then(function (reply) {
471
+				certificateUrl = reply.certificate;
472
+        BACME._logBody(reply);
473
+			});
474
+		});
475
+	});
476
+};
477
+
478
+}(window));