progress on bacme
This commit is contained in:
		
							parent
							
								
									549771a0bc
								
							
						
					
					
						commit
						4fd5fd8bd9
					
				
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1 +1,2 @@ | ||||
| js/pkijs.org | ||||
| js/browser-csr | ||||
|  | ||||
| @ -14,6 +14,9 @@ | ||||
|     <script src="./js/pkijs.org/v1.3.33/asn1.js"></script> | ||||
|     <script src="./js/pkijs.org/v1.3.33/x509_schema.js"></script> | ||||
|     <script src="./js/pkijs.org/v1.3.33/x509_simpl.js"></script> | ||||
|     <script src="./js/browser-csr/v1.0.0-alpha/csr.js"></script> | ||||
| 
 | ||||
|     <script src="./js/bacme.js"></script> | ||||
|     <script src="./js/app.js"></script> | ||||
|   </body> | ||||
| </html> | ||||
|  | ||||
| @ -1,6 +1,14 @@ | ||||
| #!/bin/bash | ||||
| 
 | ||||
| mkdir -p js/pkijs.org/v1.3.33/ | ||||
| pushd js/pkijs.org/v1.3.33/ | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/common.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_schema.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/PKI.js/41b63af760cacb565dd850fb3466ada4ca163eff/org/pkijs/x509_simpl.js | ||||
|   wget -c https://raw.githubusercontent.com/PeculiarVentures/ASN1.js/f7181c21c61e53a940ea24373ab489ad86d51bc1/org/pkijs/asn1.js | ||||
| popd | ||||
| 
 | ||||
| mkdir -p js/browser-csr/v1.0.0-alpha/ | ||||
| pushd js/browser-csr/v1.0.0-alpha/ | ||||
|   wget -c https://git.coolaj86.com/coolaj86/browser-csr.js/raw/commit/c513a862a4e016794da800f0c2eec858b80837ab/csr.js | ||||
| popd | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| (function () { | ||||
| 'use strict'; | ||||
| 
 | ||||
|   console.log("Hello, World!"); | ||||
| 
 | ||||
|   //window.document.querySelector('.js-acme-directory-url').value = 'https://acme-v02.api.letsencrypt.org/directory';
 | ||||
|   window.document.querySelector('.js-acme-directory-url').value = 'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||
| }()); | ||||
|  | ||||
							
								
								
									
										478
									
								
								js/bacme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										478
									
								
								js/bacme.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,478 @@ | ||||
| (function (exports) { | ||||
| 'use strict'; | ||||
| 
 | ||||
| var BACME = exports.BACME = {}; | ||||
| var webFetch = exports.fetch; | ||||
| var webCrypto = exports.crypto; | ||||
| 
 | ||||
| var directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory'; | ||||
| var directory; | ||||
| 
 | ||||
| var nonceUrl = directory.newNonce || 'https://acme-staging-v02.api.letsencrypt.org/acme/new-nonce'; | ||||
| var nonce; | ||||
| 
 | ||||
| var accountKeypair; | ||||
| var accountJwk; | ||||
| 
 | ||||
| var accountUrl = directory.newAccount; | ||||
| var signedAccount; | ||||
| 
 | ||||
| BACME.challengePrefixes = { | ||||
|   'http-01': '/.well-known/acme-challenge' | ||||
| , 'dns-01': '_acme-challenge' | ||||
| }; | ||||
| 
 | ||||
| BACME._logHeaders = function (resp) { | ||||
| 	console.log('Headers:'); | ||||
| 	Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); | ||||
| }; | ||||
| 
 | ||||
| BACME._logBody = function (body) { | ||||
| 	console.log('Body:'); | ||||
| 	console.log(JSON.stringify(body, null, 2)); | ||||
| 	console.log(''); | ||||
| }; | ||||
| 
 | ||||
| BACME.directory = function (url) { | ||||
| 	return webFetch(directoryUrl, { mode: 'cors' }).then(function (resp) { | ||||
| 		BACME._logHeaders(resp); | ||||
| 		return resp.json().then(function (body) { | ||||
| 			directory = body; | ||||
|       BACME._logBody(body); | ||||
|       return body; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.nonce = function () { | ||||
| 	return webFetch(nonceUrl, { mode: 'cors' }).then(function (resp) { | ||||
|     BACME._logHeaders(resp); | ||||
| 		nonce = resp.headers.get('replay-nonce'); | ||||
| 		console.log('Nonce:', nonce); | ||||
| 		// resp.body is empty
 | ||||
| 		return resp.headers.get('replay-nonce'); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.accounts = {}; | ||||
| BACME.accounts.generateKeypair = function () { | ||||
| 	// https://github.com/diafygi/webcrypto-examples#ecdsa---generatekey
 | ||||
| 	var extractable = true; | ||||
| 	return webCrypto.subtle.generateKey( | ||||
| 		{ name: "ECDSA", namedCurve: "P-256" } | ||||
| 	, extractable | ||||
| 	, [ 'sign', 'verify' ] | ||||
| 	).then(function (result) { | ||||
| 		accountKeypair = result; | ||||
| 
 | ||||
| 		return webCrypto.subtle.exportKey( | ||||
| 			"jwk" | ||||
| 		, result.privateKey | ||||
| 		).then(function (jwk) { | ||||
| 
 | ||||
| 			accountJwk = jwk; | ||||
| 			console.log('private jwk:'); | ||||
| 			console.log(JSON.stringify(jwk, null, 2)); | ||||
| 
 | ||||
| 			return webCrypto.subtle.exportKey( | ||||
| 				"pkcs8" | ||||
| 			, result.privateKey | ||||
| 			).then(function (keydata) { | ||||
| 				console.log('pkcs8:'); | ||||
| 				console.log(Array.from(new Uint8Array(keydata))); | ||||
| 
 | ||||
|         return accountKeypair; | ||||
| 			}); | ||||
| 		}) | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| // json to url-safe base64
 | ||||
| BACME._jsto64 = function (json) { | ||||
| 	return btoa(JSON.stringify(json)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| }; | ||||
| 
 | ||||
| var textEncoder = new TextEncoder(); | ||||
| 
 | ||||
| // email = john.doe@gmail.com
 | ||||
| BACME.accounts.sign = function (email) { | ||||
| 	var payload64 = BACME._jsto64( | ||||
| 		{ termsOfServiceAgreed: true | ||||
| 		, onlyReturnExisting: false | ||||
| 		, contact: [ 'mailto:' + email ] | ||||
| 		} | ||||
| 	); | ||||
| 
 | ||||
| 	var protected64 = BACME._jsto64( | ||||
| 		{ nonce: nonce | ||||
| 		, url: accountUrl | ||||
| 		, alg: 'ES256' | ||||
| 		, jwk: { | ||||
| 				kty: accountJwk.kty | ||||
| 			, crv: accountJwk.crv | ||||
| 			, x: accountJwk.x | ||||
| 			, y: accountJwk.y | ||||
| 			} | ||||
| 		} | ||||
| 	); | ||||
| 
 | ||||
| 	// Note: this function hashes before signing so send data, not the hash
 | ||||
| 	return window.crypto.subtle.sign( | ||||
| 		{ name: "ECDSA", hash: { name: "SHA-256" } } | ||||
| 	, accountKeypair.privateKey | ||||
| 	, textEncoder.encode(protected64 + '.' + payload64) | ||||
| 	).then(function (signature) { | ||||
| 
 | ||||
| 		// convert buffer to urlsafe base64
 | ||||
| 		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		console.log('URL-safe Base64 Signature:'); | ||||
| 		console.log(sig64); | ||||
| 
 | ||||
| 		signedAccount = { | ||||
| 			protected: protected64 | ||||
| 		, payload: payload64 | ||||
| 		, signature: sig64 | ||||
| 		}; | ||||
| 		console.log('Signed Base64 Account:'); | ||||
| 		console.log(JSON.stringify(signedAccount, null, 2)); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var account; | ||||
| var accountId; | ||||
| 
 | ||||
| BACME.accounts.set = function () { | ||||
| 	nonce = null; | ||||
| 	return window.fetch(accountUrl, { | ||||
| 		mode: 'cors' | ||||
| 	, method: 'POST' | ||||
| 	, headers: { 'Content-Type': 'application/jose+json' } | ||||
| 	, body: JSON.stringify(signedAccount) | ||||
| 	}).then(function (resp) { | ||||
| 		BACME._logHeaders(resp); | ||||
| 		nonce = resp.headers.get('replay-nonce'); | ||||
| 		accountId = resp.headers.get('location'); | ||||
| 		console.log('Next nonce:', nonce); | ||||
| 		console.log('Location/kid:', accountId); | ||||
| 
 | ||||
| 		if (!resp.headers.get('content-type')) { | ||||
| 		 console.log('Body: <none>'); | ||||
| 		 return; | ||||
| 		} | ||||
| 
 | ||||
| 		return resp.json().then(function (result) { | ||||
|       BACME._logBody(result); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var orderUrl = directory.newOrder || "https://acme-staging-v02.api.letsencrypt.org/acme/new-order"; | ||||
| var signedOrder; | ||||
| 
 | ||||
| BACME.orders = {}; | ||||
| 
 | ||||
| // identifiers = [ { type: 'dns', value: 'example.com' }, { type: 'dns', value: '*.example.com' } ]
 | ||||
| BACME.orders.sign = function (identifiers) { | ||||
| 	var payload64 = jsto64({ identifiers: identifiers }); | ||||
| 
 | ||||
| 	var protected64 = jsto64( | ||||
| 		{ nonce: nonce, alg: 'ES256', url: orderUrl, kid: accountId } | ||||
| 	); | ||||
| 
 | ||||
| 	return window.crypto.subtle.sign( | ||||
| 		{ name: "ECDSA", hash: { name: "SHA-256" } } | ||||
| 	, accountKeypair.privateKey | ||||
| 	, textEncoder.encode(protected64 + '.' + payload64) | ||||
| 	).then(function (signature) { | ||||
| 
 | ||||
| 		// convert buffer to urlsafe base64
 | ||||
| 		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		console.log('URL-safe Base64 Signature:'); | ||||
| 		console.log(sig64); | ||||
| 
 | ||||
| 		signedOrder = { | ||||
| 			protected: protected64 | ||||
| 		, payload: payload64 | ||||
| 		, signature: sig64 | ||||
| 		}; | ||||
| 		console.log('Signed Base64 Order:'); | ||||
| 		console.log(JSON.stringify(signedAccount, null, 2)); | ||||
| 
 | ||||
|     return signedOrder; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var order; | ||||
| var currentOrderUrl; | ||||
| var authorizationUrls; | ||||
| var finalizeUrl; | ||||
| 
 | ||||
| BACME.orders.create = function () { | ||||
| 	nonce = null; | ||||
| 	return window.fetch(orderUrl, { | ||||
| 		mode: 'cors' | ||||
| 	, method: 'POST' | ||||
| 	, headers: { 'Content-Type': 'application/jose+json' } | ||||
| 	, body: JSON.stringify(signedOrder) | ||||
| 	}).then(function (resp) { | ||||
| 		console.log('Headers:'); | ||||
| 		Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); | ||||
| 		currentOrderUrl = resp.headers.get('location'); | ||||
| 		nonce = resp.headers.get('replay-nonce'); | ||||
| 		console.log('Next nonce:', nonce); | ||||
| 
 | ||||
| 		return resp.json().then(function (result) { | ||||
| 			authorizationUrls = result.authorizations; | ||||
| 			finalizeUrl = result.finalize; | ||||
| 			console.log('Body:'); | ||||
| 			console.log(JSON.stringify(result, null, 2)); | ||||
| 
 | ||||
|       return result; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.challenges = {}; | ||||
| BACME.challenges.view = function () { | ||||
| 	var authzUrl = authorizationUrls.pop(); | ||||
| 	var token; | ||||
| 	var challengeDomain; | ||||
| 	var challengeUrl; | ||||
| 
 | ||||
| 	return window.fetch(authzUrl, { | ||||
| 		mode: 'cors' | ||||
| 	}).then(function (resp) { | ||||
|     BACME._logHeaders(resp); | ||||
| 
 | ||||
| 		return resp.json().then(function (result) { | ||||
| 			// Note: select the challenge you wish to use
 | ||||
| 			var challenge = result.challenges.slice(0).pop(); | ||||
| 			token = challenge.token; | ||||
| 			challengeUrl = challenge.url; | ||||
| 			challengeDomain = result.identifier.value; | ||||
| 
 | ||||
|       BACME._logBody(result); | ||||
| 
 | ||||
|       return { token: challenge.token, url: challenge.url, domain: result.identifier.value, challenges: result.challenges }; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var thumbprint; | ||||
| var keyAuth; | ||||
| var httpPath; | ||||
| var dnsAuth; | ||||
| var dnsRecord; | ||||
| 
 | ||||
| BACME.thumbprint = function () { | ||||
| 	// https://stackoverflow.com/questions/42588786/how-to-fingerprint-a-jwk
 | ||||
| 
 | ||||
| 	var accountPublicStr = '{' + ['crv', 'kty', 'x', 'y'].map(function (key) { | ||||
| 		return '"' + key + '":"' + accountJwk[key] + '"'; | ||||
| 	}).join(',') + '}'; | ||||
| 
 | ||||
| 	return window.crypto.subtle.digest( | ||||
| 		{ name: "SHA-256" } // SHA-256 is spec'd, non-optional
 | ||||
| 	, textEncoder.encode(accountPublicStr) | ||||
| 	).then(function(hash){ | ||||
| 		thumbprint = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		console.log('Thumbprint:'); | ||||
| 		console.log(thumbprint); | ||||
| 
 | ||||
|     return thumbprint; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.challenges['http-01'] = function () { | ||||
| 	// The contents of the key authorization file
 | ||||
| 	keyAuth = token + '.' + thumbprint; | ||||
| 
 | ||||
| 	// Where the key authorization file goes
 | ||||
| 	httpPath = 'http://' + challengeDomain + '/.well-known/acme-challenge/' + token; | ||||
| 
 | ||||
|   console.log("echo '" + keyAuth + "' > '" + httpPath + "'"); | ||||
| 
 | ||||
|   return { | ||||
|     path: httpPath | ||||
|   , value: keyAuth | ||||
|   }; | ||||
| }); | ||||
| BACME.challenges['dns-01'] = function () { | ||||
| 	return window.crypto.subtle.digest( | ||||
| 		{ name: "SHA-256", } | ||||
| 	, textEncoder.encode(keyAuth) | ||||
| 	).then(function(hash){ | ||||
| 		dnsAuth = btoa(Array.prototype.map.call(new Uint8Array(hash), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		dnsRecord = '_acme-challenge.' + challengeDomain; | ||||
| 
 | ||||
| 		console.log('DNS TXT Auth:'); | ||||
| 		// The name of the record
 | ||||
| 		console.log(dnsRecord); | ||||
| 		// The TXT record value
 | ||||
| 		console.log(dnsAuth); | ||||
| 
 | ||||
|     return { | ||||
|       type: 'TXT' | ||||
|     , host: dnsRecord | ||||
|     , answer: dnsAuth; | ||||
|     }; | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var challengePollUrl; | ||||
| 
 | ||||
| BACME.challenges.accept = function () { | ||||
|   var payload64 = jsto64( | ||||
| 		{} | ||||
| 	); | ||||
| 
 | ||||
| 	var protected64 = jsto64( | ||||
| 		{ nonce: nonce, alg: 'ES256', url: challengeUrl, kid: accountId } | ||||
| 	); | ||||
| 
 | ||||
| 	nonce = null; | ||||
| 	return window.crypto.subtle.sign( | ||||
| 		{ name: "ECDSA", hash: { name: "SHA-256" } } | ||||
| 	, accountKeypair.privateKey | ||||
| 	, textEncoder.encode(protected64 + '.' + payload64) | ||||
| 	).then(function (signature) { | ||||
| 
 | ||||
| 		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		var body = { | ||||
| 			protected: protected64 | ||||
| 		, payload: payload64 | ||||
| 		, signature: sig64 | ||||
| 		}; | ||||
| 
 | ||||
| 		return window.fetch( | ||||
| 			challengeUrl | ||||
| 		, { mode: 'cors' | ||||
| 			, method: 'POST' | ||||
| 			, headers: { 'Content-Type': 'application/jose+json' } | ||||
| 			, body: JSON.stringify(body) | ||||
| 			} | ||||
| 		).then(function (resp) { | ||||
| 			console.log('Headers:'); | ||||
| 			Array.from(resp.headers.entries()).forEach(function (h) { console.log(h[0] + ': ' + h[1]); }); | ||||
| 			nonce = resp.headers.get('replay-nonce'); | ||||
| 
 | ||||
| 			return resp.json().then(function (reply) { | ||||
| 				challengePollUrl = reply.url; | ||||
| 
 | ||||
| 				console.log('Challenge ACK:'); | ||||
| 				console.log(JSON.stringify(reply)); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.challenges.check = function () { | ||||
| 	return window.fetch(challengePollUrl, { mode: 'cors' }).then(function (resp) { | ||||
|     BACME._logHeaders(resp); | ||||
| 		nonce = resp.headers.get('replay-nonce'); | ||||
| 
 | ||||
| 		return resp.json().then(function (reply) { | ||||
| 			challengePollUrl = reply.url; | ||||
| 
 | ||||
|       BACME._logBody(reply); | ||||
| 
 | ||||
| 			return reply; | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| var domainKeypair; | ||||
| var domainJwk; | ||||
| 
 | ||||
| BACME.domains = {}; | ||||
| // TODO factor out from BACME.accounts.generateKeypair
 | ||||
| BACME.domains.generateKeypair = function () { | ||||
| 	var extractable = true; | ||||
| 	return window.crypto.subtle.generateKey( | ||||
| 		{ name: "ECDSA", namedCurve: "P-256" } | ||||
| 	, extractable | ||||
| 	, [ 'sign', 'verify' ] | ||||
| 	).then(function (result) { | ||||
| 		domainKeypair = result; | ||||
| 
 | ||||
| 		return window.crypto.subtle.exportKey( | ||||
| 			"jwk" | ||||
| 		, result.privateKey | ||||
| 		).then(function (jwk) { | ||||
| 
 | ||||
| 			domainJwk = jwk; | ||||
| 			console.log('private jwk:'); | ||||
| 			console.log(JSON.stringify(jwk, null, 2)); | ||||
| 
 | ||||
|       return domainKeypair; | ||||
| 		}) | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| BACME.order.generateCsr = function (keypair, domains) { | ||||
|   return Promise.resolve(CSR.generate(keypair, domains)); | ||||
| }; | ||||
| 
 | ||||
| var certificateUrl; | ||||
| 
 | ||||
| BACME.order.finalize = function () { | ||||
| 	var payload64 = jsto64( | ||||
| 		{ csr: csr } | ||||
| 	); | ||||
| 
 | ||||
| 	var protected64 = jsto64( | ||||
| 		{ nonce: nonce, alg: 'ES256', url: finalizeUrl, kid: accountId } | ||||
| 	); | ||||
| 
 | ||||
| 	nonce = null; | ||||
| 	return window.crypto.subtle.sign( | ||||
| 		{ name: "ECDSA", hash: { name: "SHA-256" } } | ||||
| 	, accountKeypair.privateKey | ||||
| 	, textEncoder.encode(protected64 + '.' + payload64) | ||||
| 	).then(function (signature) { | ||||
| 
 | ||||
| 		var sig64 = btoa(Array.prototype.map.call(new Uint8Array(signature), function (ch) { | ||||
| 			return String.fromCharCode(ch); | ||||
| 		}).join('')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); | ||||
| 
 | ||||
| 		var body = { | ||||
| 			protected: protected64 | ||||
| 		, payload: payload64 | ||||
| 		, signature: sig64 | ||||
| 		}; | ||||
| 
 | ||||
| 		return window.fetch( | ||||
| 			finalizeUrl | ||||
| 		, { mode: 'cors' | ||||
| 			, method: 'POST' | ||||
| 			, headers: { 'Content-Type': 'application/jose+json' } | ||||
| 			, body: JSON.stringify(body) | ||||
| 			} | ||||
| 		).then(function (resp) { | ||||
|       BACME._logHeaders(resp); | ||||
| 			nonce = resp.headers.get('replay-nonce'); | ||||
| 
 | ||||
| 			return resp.json().then(function (reply) { | ||||
| 				certificateUrl = reply.certificate; | ||||
|         BACME._logBody(reply); | ||||
| 			}); | ||||
| 		}); | ||||
| 	}); | ||||
| }; | ||||
| 
 | ||||
| }(window)); | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user