mirror of
				https://github.com/therootcompany/acme.js.git
				synced 2024-11-16 17:29:00 +00:00 
			
		
		
		
	Can register new ECDSA or RSA account, huzzah!
This commit is contained in:
		
							parent
							
								
									76621560cb
								
							
						
					
					
						commit
						488067ec20
					
				
							
								
								
									
										18
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								app.js
									
									
									
									
									
								
							| @ -5,6 +5,7 @@ | ||||
|   var Rasha = window.Rasha; | ||||
|   var Eckles = window.Eckles; | ||||
|   var x509 = window.x509; | ||||
|   var ACME = window.ACME; | ||||
| 
 | ||||
|   function $(sel) { | ||||
|     return document.querySelector(sel); | ||||
| @ -106,7 +107,22 @@ | ||||
|       ev.preventDefault(); | ||||
|       ev.stopPropagation(); | ||||
|       $('.js-loading').hidden = false; | ||||
|       //ACME.accounts.create
 | ||||
|       var acme = ACME.create({ | ||||
|         Keypairs: Keypairs | ||||
|       }); | ||||
|       acme.init('https://acme-staging-v02.api.letsencrypt.org/directory').then(function (result) { | ||||
|         console.log('acme result', result); | ||||
|         return acme.accounts.create({ | ||||
|           email: $('.js-email').innerText | ||||
|         , agreeToTerms: function (tos) { | ||||
|             console.log("TODO checkbox for agree to terms"); | ||||
|             return tos; | ||||
|           } | ||||
|         , accountKeypair: { | ||||
|             privateKeyJwk: JSON.parse($('.js-jwk').innerText).private | ||||
|           } | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     $('.js-generate').hidden = false; | ||||
|  | ||||
							
								
								
									
										652
									
								
								lib/acme.js
									
									
									
									
									
								
							
							
						
						
									
										652
									
								
								lib/acme.js
									
									
									
									
									
								
							| @ -30,7 +30,7 @@ ACME.challengePrefixes = { | ||||
| ACME.challengeTests = { | ||||
|   'http-01': function (me, auth) { | ||||
|     var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||
|     return me._request({ method: 'GET', url: url }).then(function (resp) { | ||||
|     return me.request({ method: 'GET', url: url }).then(function (resp) { | ||||
|       var err; | ||||
| 
 | ||||
|       // TODO limit the number of bytes that are allowed to be downloaded
 | ||||
| @ -76,16 +76,28 @@ ACME.challengeTests = { | ||||
| 
 | ||||
| ACME._directory = function (me) { | ||||
|   // GET-as-GET ok
 | ||||
|   return me._request({ method: 'GET', url: me.directoryUrl, json: true }); | ||||
|   return me.request({ method: 'GET', url: me.directoryUrl, json: true }); | ||||
| }; | ||||
| ACME._getNonce = function (me) { | ||||
|   // GET-as-GET, HEAD-as-HEAD ok
 | ||||
|   if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } | ||||
|   return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { | ||||
|     me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|     return me._nonce; | ||||
|   var nonce; | ||||
|   while (true) { | ||||
|     nonce = me._nonces.shift(); | ||||
|     if (!nonce) { break; } | ||||
|     if (Date.now() - nonce.createdAt > (15 * 60 * 1000)) { | ||||
|       nonce = null; | ||||
|     } else { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (nonce) { return Promise.resolve(nonce); } | ||||
|   return me.request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { | ||||
|     return resp.headers['replay-nonce']; | ||||
|   }); | ||||
| }; | ||||
| ACME._setNonce = function (me, nonce) { | ||||
|   me._nonces.unshift({ nonce: nonce, createdAt: Date.now() }); | ||||
| }; | ||||
| // ACME RFC Section 7.3 Account Creation
 | ||||
| /* | ||||
|  { | ||||
| @ -109,91 +121,86 @@ ACME._getNonce = function (me) { | ||||
| ACME._registerAccount = function (me, options) { | ||||
|   if (me.debug) { console.debug('[acme-v2] accounts.create'); } | ||||
| 
 | ||||
|   return ACME._getNonce(me).then(function () { | ||||
|     return new Promise(function (resolve, reject) { | ||||
|   return new Promise(function (resolve, reject) { | ||||
| 
 | ||||
|       function agree(tosUrl) { | ||||
|         var err; | ||||
|         if (me._tos !== tosUrl) { | ||||
|           err = new Error("You must agree to the ToS at '" + me._tos + "'"); | ||||
|           err.code = "E_AGREE_TOS"; | ||||
|           reject(err); | ||||
|           return; | ||||
|         } | ||||
|     function agree(tosUrl) { | ||||
|       var err; | ||||
|       if (me._tos !== tosUrl) { | ||||
|         err = new Error("You must agree to the ToS at '" + me._tos + "'"); | ||||
|         err.code = "E_AGREE_TOS"; | ||||
|         reject(err); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|         var jwk = options.accountKeypair.privateKeyJwk; | ||||
|         var p; | ||||
|         if (jwk) { | ||||
|           p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); | ||||
|         } else { | ||||
|           p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||
|       var jwk = options.accountKeypair.privateKeyJwk; | ||||
|       var p; | ||||
|       if (jwk) { | ||||
|         // nix the browser jwk extras
 | ||||
|         jwk.key_ops = undefined; | ||||
|         jwk.ext = undefined; | ||||
|         p = Promise.resolve({ private: jwk, public: Keypairs.neuter({ jwk: jwk }) }); | ||||
|       } else { | ||||
|         p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||
|       } | ||||
|       return p.then(function (pair) { | ||||
|         options.accountKeypair.privateKeyJwk = pair.private; | ||||
|         options.accountKeypair.publicKeyJwk = pair.public; | ||||
|         if (pair.public.kid) { | ||||
|           pair = JSON.parse(JSON.stringify(pair)); | ||||
|           delete pair.public.kid; | ||||
|           delete pair.private.kid; | ||||
|         } | ||||
|         return p.then(function (pair) { | ||||
|           if (pair.public.kid) { | ||||
|             pair = JSON.parse(JSON.stringify(pair)); | ||||
|             delete pair.public.kid; | ||||
|             delete pair.private.kid; | ||||
|           } | ||||
|           return pair; | ||||
|         }).then(function (pair) { | ||||
|           var contact; | ||||
|           if (options.contact) { | ||||
|             contact = options.contact.slice(0); | ||||
|           } else if (options.email) { | ||||
|             contact = [ 'mailto:' + options.email ]; | ||||
|           } | ||||
|           var body = { | ||||
|             termsOfServiceAgreed: tosUrl === me._tos | ||||
|           , onlyReturnExisting: false | ||||
|           , contact: contact | ||||
|           }; | ||||
|           if (options.externalAccount) { | ||||
|             body.externalAccountBinding = me.RSA.signJws( | ||||
|               // TODO is HMAC the standard, or is this arbitrary?
 | ||||
|               options.externalAccount.secret | ||||
|             , undefined | ||||
|             , { alg: options.externalAccount.alg || "HS256" | ||||
|               , kid: options.externalAccount.id | ||||
|               , url: me._directoryUrls.newAccount | ||||
|               } | ||||
|             , Buffer.from(JSON.stringify(pair.public)) | ||||
|             ); | ||||
|           } | ||||
|           var payload = JSON.stringify(body); | ||||
|           var jws = Keypairs.signJws( | ||||
|             options.accountKeypair | ||||
|           , undefined | ||||
|           , { nonce: me._nonce | ||||
|             , alg: (me._alg || 'RS256') | ||||
|         return pair; | ||||
|       }).then(function (pair) { | ||||
|         var contact; | ||||
|         if (options.contact) { | ||||
|           contact = options.contact.slice(0); | ||||
|         } else if (options.email) { | ||||
|           contact = [ 'mailto:' + options.email ]; | ||||
|         } | ||||
|         var body = { | ||||
|           termsOfServiceAgreed: tosUrl === me._tos | ||||
|         , onlyReturnExisting: false | ||||
|         , contact: contact | ||||
|         }; | ||||
|         var pExt; | ||||
|         if (options.externalAccount) { | ||||
|           pExt = me.Keypairs.signJws({ | ||||
|             // TODO is HMAC the standard, or is this arbitrary?
 | ||||
|             secret: options.externalAccount.secret | ||||
|           , protected: { | ||||
|               alg: options.externalAccount.alg || "HS256" | ||||
|             , kid: options.externalAccount.id | ||||
|             , url: me._directoryUrls.newAccount | ||||
|             , jwk: pair.public | ||||
|             } | ||||
|           , Buffer.from(payload) | ||||
|           ); | ||||
| 
 | ||||
|           delete jws.header; | ||||
|           if (me.debug) { console.debug('[acme-v2] accounts.create JSON body:'); } | ||||
|           if (me.debug) { console.debug(jws); } | ||||
|           me._nonce = null; | ||||
|           return me._request({ | ||||
|             method: 'POST' | ||||
|           , payload: Enc.strToBuf(JSON.stringify(pair.public)) | ||||
|           }).then(function (jws) { | ||||
|             body.externalAccountBinding = jws; | ||||
|             return body; | ||||
|           }); | ||||
|         } else { | ||||
|           pExt = Promise.resolve(body); | ||||
|         } | ||||
|         return pExt.then(function (body) { | ||||
|           var payload = JSON.stringify(body); | ||||
|           return ACME._jwsRequest(me, { | ||||
|             options: options | ||||
|           , url: me._directoryUrls.newAccount | ||||
|           , headers: { 'Content-Type': 'application/jose+json' } | ||||
|           , json: jws | ||||
|           , protected: { kid: false, jwk: pair.public } | ||||
|           , payload: Enc.binToBuf(payload) | ||||
|           }).then(function (resp) { | ||||
|             var account = resp.body; | ||||
| 
 | ||||
|             if (2 !== Math.floor(resp.statusCode / 100)) { | ||||
|               throw new Error('account error: ' + JSON.stringify(body)); | ||||
|               throw new Error('account error: ' + JSON.stringify(resp.body)); | ||||
|             } | ||||
| 
 | ||||
|             me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|             var location = resp.toJSON().headers.location; | ||||
|             var location = resp.headers.location; | ||||
|             // the account id url
 | ||||
|             me._kid = location; | ||||
|             options._kid = location; | ||||
|             if (me.debug) { console.debug('[DEBUG] new account location:'); } | ||||
|             if (me.debug) { console.debug(location); } | ||||
|             if (me.debug) { console.debug(resp.toJSON()); } | ||||
|             if (me.debug) { console.debug(resp); } | ||||
| 
 | ||||
|             /* | ||||
|             { | ||||
| @ -205,29 +212,29 @@ ACME._registerAccount = function (me, options) { | ||||
|             if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||
|             // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | ||||
|             if (!account.key) { account.key = {}; } | ||||
|             account.key.kid = me._kid; | ||||
|             account.key.kid = options._kid; | ||||
|             return account; | ||||
|           }).then(resolve, reject); | ||||
|         }); | ||||
|       } | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|       if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } | ||||
|       if (1 === options.agreeToTerms.length) { | ||||
|         // newer promise API
 | ||||
|         return options.agreeToTerms(me._tos).then(agree, reject); | ||||
|       } | ||||
|       else if (2 === options.agreeToTerms.length) { | ||||
|         // backwards compat cb API
 | ||||
|         return options.agreeToTerms(me._tos, function (err, tosUrl) { | ||||
|           if (!err) { agree(tosUrl); return; } | ||||
|           reject(err); | ||||
|         }); | ||||
|       } | ||||
|       else { | ||||
|         reject(new Error('agreeToTerms has incorrect function signature.' | ||||
|           + ' Should be fn(tos) { return Promise<tos>; }')); | ||||
|       } | ||||
|     }); | ||||
|     if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } | ||||
|     if (1 === options.agreeToTerms.length) { | ||||
|       // newer promise API
 | ||||
|       return Promise.resolve(options.agreeToTerms(me._tos)).then(agree, reject); | ||||
|     } | ||||
|     else if (2 === options.agreeToTerms.length) { | ||||
|       // backwards compat cb API
 | ||||
|       return options.agreeToTerms(me._tos, function (err, tosUrl) { | ||||
|         if (!err) { agree(tosUrl); return; } | ||||
|         reject(err); | ||||
|       }); | ||||
|     } | ||||
|     else { | ||||
|       reject(new Error('agreeToTerms has incorrect function signature.' | ||||
|         + ' Should be fn(tos) { return Promise<tos>; }')); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| /* | ||||
| @ -250,10 +257,16 @@ ACME._registerAccount = function (me, options) { | ||||
|    "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" | ||||
|  } | ||||
| */ | ||||
| ACME._getChallenges = function (me, options, auth) { | ||||
| ACME._getChallenges = function (me, options, authUrl) { | ||||
|   if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } | ||||
|   // TODO POST-as-GET
 | ||||
|   return me._request({ method: 'GET', url: auth, json: true }).then(function (resp) { | ||||
| 
 | ||||
|   return ACME._jwsRequest(me, { | ||||
|     options: options | ||||
|   , protected: {} | ||||
|   , payload: '' | ||||
|   , url: authUrl | ||||
|   }).then(function (resp) { | ||||
|     return resp.body; | ||||
|   }); | ||||
| }; | ||||
| @ -389,16 +402,18 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | ||||
|   auth.hostname = auth.identifier.value; | ||||
|   // because I'm not 100% clear if the wildcard identifier does or doesn't have the leading *. in all cases
 | ||||
|   auth.altname = ACME._untame(auth.identifier.value, auth.wildcard); | ||||
|   auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); | ||||
|   //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||
|   auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||
|   // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||
|   auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||
|   auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||
|   return me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).then(function (thumb) { | ||||
|     auth.thumbprint = thumb; | ||||
|     //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||
|     auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||
|     // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||
|     auth.challengeUrl = 'http://' + auth.identifier.value + ACME.challengePrefixes['http-01'] + '/' + auth.token; | ||||
|     auth.dnsHost = dnsPrefix + '.' + auth.hostname.replace('*.', ''); | ||||
| 
 | ||||
|   return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { | ||||
|     auth.dnsAuthorization = hash; | ||||
|     return auth; | ||||
|     return Crypto._sha('sha256', auth.keyAuthorization).then(function (hash) { | ||||
|       auth.dnsAuthorization = hash; | ||||
|       return auth; | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| @ -436,25 +451,13 @@ ACME._postChallenge = function (me, options, auth) { | ||||
|    } | ||||
|    */ | ||||
|   function deactivate() { | ||||
|     var jws = me.RSA.signJws( | ||||
|       options.accountKeypair | ||||
|     , undefined | ||||
|     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||||
|     , Buffer.from(JSON.stringify({ "status": "deactivated" })) | ||||
|     ); | ||||
|     me._nonce = null; | ||||
|     return me._request({ | ||||
|       method: 'POST' | ||||
|     if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
|     , url: auth.url | ||||
|     , headers: { 'Content-Type': 'application/jose+json' } | ||||
|     , json: jws | ||||
|     , protected: { kid: options._kid } | ||||
|     , payload: Enc.strToBuf(JSON.stringify({ "status": "deactivated" })) | ||||
|     }).then(function (resp) { | ||||
|       if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } | ||||
|       if (me.debug) { console.debug(resp.headers); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
|       if (me.debug) { console.debug(); } | ||||
| 
 | ||||
|       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
|       return ACME._wait(DEAUTH_INTERVAL); | ||||
| @ -472,7 +475,7 @@ ACME._postChallenge = function (me, options, auth) { | ||||
| 
 | ||||
|     if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } | ||||
|     // TODO POST-as-GET
 | ||||
|     return me._request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { | ||||
|     return me.request({ method: 'GET', url: auth.url, json: true }).then(function (resp) { | ||||
|       if ('processing' === resp.body.status) { | ||||
|         if (me.debug) { console.debug('poll: again'); } | ||||
|         return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||
| @ -523,25 +526,13 @@ ACME._postChallenge = function (me, options, auth) { | ||||
|   } | ||||
| 
 | ||||
|   function respondToChallenge() { | ||||
|     var jws = me.RSA.signJws( | ||||
|       options.accountKeypair | ||||
|     , undefined | ||||
|     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: auth.url, kid: me._kid } | ||||
|     , Buffer.from(JSON.stringify({ })) | ||||
|     ); | ||||
|     me._nonce = null; | ||||
|     return me._request({ | ||||
|       method: 'POST' | ||||
|     if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
|     , url: auth.url | ||||
|     , headers: { 'Content-Type': 'application/jose+json' } | ||||
|     , json: jws | ||||
|     , protected: { kid: options._kid } | ||||
|     , payload: Enc.strToBuf(JSON.stringify({})) | ||||
|     }).then(function (resp) { | ||||
|       if (me.debug) { console.debug('[acme-v2.js] challenge accepted!'); } | ||||
|       if (me.debug) { console.debug(resp.headers); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
|       if (me.debug) { console.debug(); } | ||||
| 
 | ||||
|       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|       if (me.debug) { console.debug('respond to challenge: resp.body:'); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
|       return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||
| @ -586,36 +577,26 @@ ACME._setChallenge = function (me, options, auth) { | ||||
| }; | ||||
| ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||
|   if (me.debug) { console.debug('finalizeOrder:'); } | ||||
|   var csr = me.RSA.generateCsrWeb64(options.domainKeypair, validatedDomains); | ||||
|   var csr = me.Keypairs.generateCsrWeb64(options.domainKeypair, validatedDomains); | ||||
|   var body = { csr: csr }; | ||||
|   var payload = JSON.stringify(body); | ||||
| 
 | ||||
|   function pollCert() { | ||||
|     var jws = me.RSA.signJws( | ||||
|       options.accountKeypair | ||||
|     , undefined | ||||
|     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } | ||||
|     , Buffer.from(payload) | ||||
|     ); | ||||
| 
 | ||||
|     if (me.debug) { console.debug('finalize:', me._finalize); } | ||||
|     me._nonce = null; | ||||
|     return me._request({ | ||||
|       method: 'POST' | ||||
|     , url: me._finalize | ||||
|     , headers: { 'Content-Type': 'application/jose+json' } | ||||
|     , json: jws | ||||
|     if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
|     , url: options._finalize | ||||
|     , protected: { kid: options._kid } | ||||
|     , payload: Enc.strToBuf(payload) | ||||
|     }).then(function (resp) { | ||||
|       // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
 | ||||
|       // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
 | ||||
|       me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
| 
 | ||||
|       if (me.debug) { console.debug('order finalized: resp.body:'); } | ||||
|       if (me.debug) { console.debug(resp.body); } | ||||
| 
 | ||||
|       // https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.1.3
 | ||||
|       // Possible values are: "pending" => ("invalid" || "ready") => "processing" => "valid"
 | ||||
|       if ('valid' === resp.body.status) { | ||||
|         me._expires = resp.body.expires; | ||||
|         me._certificate = resp.body.certificate; | ||||
|         options._expires = resp.body.expires; | ||||
|         options._certificate = resp.body.certificate; | ||||
| 
 | ||||
|         return resp.body; // return order
 | ||||
|       } | ||||
| @ -672,6 +653,11 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||
| 
 | ||||
|   return pollCert(); | ||||
| }; | ||||
| // _kid
 | ||||
| // registerAccount
 | ||||
| // postChallenge
 | ||||
| // finalizeOrder
 | ||||
| // getCertificate
 | ||||
| ACME._getCertificate = function (me, options) { | ||||
|   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } | ||||
| 
 | ||||
| @ -704,139 +690,126 @@ ACME._getCertificate = function (me, options) { | ||||
|   } | ||||
| 
 | ||||
|   // It's just fine if there's no account, we'll go get the key id we need via the public key
 | ||||
|   if (!me._kid) { | ||||
|     if (options.accountKid || options.account && options.account.kid) { | ||||
|       me._kid = options.accountKid || options.account.kid; | ||||
|     } else { | ||||
|       //return Promise.reject(new Error("must include KeyID"));
 | ||||
|       // This is an idempotent request. It'll return the same account for the same public key.
 | ||||
|       return ACME._registerAccount(me, options).then(function () { | ||||
|         // start back from the top
 | ||||
|         return ACME._getCertificate(me, options); | ||||
|       }); | ||||
|     } | ||||
|   if (options.accountKid || options.account && options.account.kid) { | ||||
|     options._kid = options.accountKid || options.account.kid; | ||||
|   } else { | ||||
|     //return Promise.reject(new Error("must include KeyID"));
 | ||||
|     // This is an idempotent request. It'll return the same account for the same public key.
 | ||||
|     return ACME._registerAccount(me, options).then(function () { | ||||
|       // start back from the top
 | ||||
|       return ACME._getCertificate(me, options); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Do a little dry-run / self-test
 | ||||
|   return ACME._testChallenges(me, options).then(function () { | ||||
|     if (me.debug) { console.debug('[acme-v2] certificates.create'); } | ||||
|     return ACME._getNonce(me).then(function () { | ||||
|       var body = { | ||||
|         // raw wildcard syntax MUST be used here
 | ||||
|         identifiers: options.domains.sort(function (a, b) { | ||||
|           // the first in the list will be the subject of the certificate, I believe (and hope)
 | ||||
|           if (!options.subject) { return 0; } | ||||
|           if (options.subject === a) { return -1; } | ||||
|           if (options.subject === b) { return 1; } | ||||
|           return 0; | ||||
|         }).map(function (hostname) { | ||||
|           return { type: "dns", value: hostname }; | ||||
|         }) | ||||
|         //, "notBefore": "2016-01-01T00:00:00Z"
 | ||||
|         //, "notAfter": "2016-01-08T00:00:00Z"
 | ||||
|       }; | ||||
|     var body = { | ||||
|       // raw wildcard syntax MUST be used here
 | ||||
|       identifiers: options.domains.sort(function (a, b) { | ||||
|         // the first in the list will be the subject of the certificate, I believe (and hope)
 | ||||
|         if (!options.subject) { return 0; } | ||||
|         if (options.subject === a) { return -1; } | ||||
|         if (options.subject === b) { return 1; } | ||||
|         return 0; | ||||
|       }).map(function (hostname) { | ||||
|         return { type: "dns", value: hostname }; | ||||
|       }) | ||||
|       //, "notBefore": "2016-01-01T00:00:00Z"
 | ||||
|       //, "notAfter": "2016-01-08T00:00:00Z"
 | ||||
|     }; | ||||
| 
 | ||||
|       var payload = JSON.stringify(body); | ||||
|       // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||
|       me._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); | ||||
|       me._alg = ('EC' === me._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||
|       var jws = me.RSA.signJws( | ||||
|         options.accountKeypair | ||||
|       , undefined | ||||
|       , { nonce: me._nonce, alg: me._alg, url: me._directoryUrls.newOrder, kid: me._kid } | ||||
|       , Buffer.from(payload, 'utf8') | ||||
|       ); | ||||
|     var payload = JSON.stringify(body); | ||||
|     // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 | ||||
|     options._kty = (options.accountKeypair.privateKeyJwk && options.accountKeypair.privateKeyJwk.kty || 'RSA'); | ||||
|     options._alg = ('EC' === options._kty) ? 'ES256' : 'RS256'; // TODO vary with bitwidth of key (if not handled)
 | ||||
|     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||
|     return ACME._jwsRequest({ | ||||
|       options: options | ||||
|     , url: me._directoryUrls.newOrder | ||||
|     , protected: { kid: options._kid } | ||||
|     , payload: Enc.strToBuf(payload) | ||||
|     }).then(function (resp) { | ||||
|       var location = resp.headers.location; | ||||
|       var setAuths; | ||||
|       var auths = []; | ||||
|       if (me.debug) { console.debug('[ordered]', location); } // the account id url
 | ||||
|       if (me.debug) { console.debug(resp); } | ||||
|       options._authorizations = resp.body.authorizations; | ||||
|       options._order = location; | ||||
|       options._finalize = resp.body.finalize; | ||||
|       //if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
 | ||||
| 
 | ||||
|       if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||
|       me._nonce = null; | ||||
|       return me._request({ | ||||
|         method: 'POST' | ||||
|       , url: me._directoryUrls.newOrder | ||||
|       , headers: { 'Content-Type': 'application/jose+json' } | ||||
|       , json: jws | ||||
|       }).then(function (resp) { | ||||
|         me._nonce = resp.toJSON().headers['replay-nonce']; | ||||
|         var location = resp.toJSON().headers.location; | ||||
|         var setAuths; | ||||
|         var auths = []; | ||||
|         if (me.debug) { console.debug(location); } // the account id url
 | ||||
|         if (me.debug) { console.debug(resp.toJSON()); } | ||||
|         me._authorizations = resp.body.authorizations; | ||||
|         me._order = location; | ||||
|         me._finalize = resp.body.finalize; | ||||
|         //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
 | ||||
|       if (!options._authorizations) { | ||||
|         return Promise.reject(new Error( | ||||
|           "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" | ||||
|           + JSON.stringify(resp.body) | ||||
|         )); | ||||
|       } | ||||
|       if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } | ||||
|       setAuths = options._authorizations.slice(0); | ||||
| 
 | ||||
|         if (!me._authorizations) { | ||||
|           return Promise.reject(new Error( | ||||
|             "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" | ||||
|             + JSON.stringify(resp.body) | ||||
|           )); | ||||
|         } | ||||
|         if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } | ||||
|         setAuths = me._authorizations.slice(0); | ||||
|       function setNext() { | ||||
|         var authUrl = setAuths.shift(); | ||||
|         if (!authUrl) { return; } | ||||
| 
 | ||||
|         function setNext() { | ||||
|           var authUrl = setAuths.shift(); | ||||
|           if (!authUrl) { return; } | ||||
|         return ACME._getChallenges(me, options, authUrl).then(function (results) { | ||||
|           // var domain = options.domains[i]; // results.identifier.value
 | ||||
| 
 | ||||
|           return ACME._getChallenges(me, options, authUrl).then(function (results) { | ||||
|             // var domain = options.domains[i]; // results.identifier.value
 | ||||
|           // If it's already valid, we're golden it regardless
 | ||||
|           if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { | ||||
|             return setNext(); | ||||
|           } | ||||
| 
 | ||||
|             // If it's already valid, we're golden it regardless
 | ||||
|             if (results.challenges.some(function (ch) { return 'valid' === ch.status; })) { | ||||
|               return setNext(); | ||||
|             } | ||||
|           var challenge = ACME._chooseChallenge(options, results); | ||||
|           if (!challenge) { | ||||
|             // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||
|             return Promise.reject(new Error( | ||||
|               "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." | ||||
|             )); | ||||
|           } | ||||
| 
 | ||||
|             var challenge = ACME._chooseChallenge(options, results); | ||||
|             if (!challenge) { | ||||
|               // For example, wildcards require dns-01 and, if we don't have that, we have to bail
 | ||||
|               return Promise.reject(new Error( | ||||
|                 "Server didn't offer any challenge we can handle for '" + options.domains.join() + "'." | ||||
|               )); | ||||
|             } | ||||
| 
 | ||||
|             return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { | ||||
|               auths.push(auth); | ||||
|               return ACME._setChallenge(me, options, auth).then(setNext); | ||||
|             }); | ||||
|           return ACME._challengeToAuth(me, options, results, challenge, false).then(function (auth) { | ||||
|             auths.push(auth); | ||||
|             return ACME._setChallenge(me, options, auth).then(setNext); | ||||
|           }); | ||||
|         } | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|         function challengeNext() { | ||||
|           var auth = auths.shift(); | ||||
|           if (!auth) { return; } | ||||
|           return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||
|         } | ||||
|       function challengeNext() { | ||||
|         var auth = auths.shift(); | ||||
|         if (!auth) { return; } | ||||
|         return ACME._postChallenge(me, options, auth).then(challengeNext); | ||||
|       } | ||||
| 
 | ||||
|         // First we set every challenge
 | ||||
|         // Then we ask for each challenge to be checked
 | ||||
|         // Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||
|         return setNext().then(challengeNext).then(function () { | ||||
|           if (me.debug) { console.debug("[getCertificate] next.then"); } | ||||
|           var validatedDomains = body.identifiers.map(function (ident) { | ||||
|             return ident.value; | ||||
|           }); | ||||
|       // First we set every challenge
 | ||||
|       // Then we ask for each challenge to be checked
 | ||||
|       // Doing otherwise would potentially cause us to poison our own DNS cache with misses
 | ||||
|       return setNext().then(challengeNext).then(function () { | ||||
|         if (me.debug) { console.debug("[getCertificate] next.then"); } | ||||
|         var validatedDomains = body.identifiers.map(function (ident) { | ||||
|           return ident.value; | ||||
|         }); | ||||
| 
 | ||||
|           return ACME._finalizeOrder(me, options, validatedDomains); | ||||
|         }).then(function (order) { | ||||
|           if (me.debug) { console.debug('acme-v2: order was finalized'); } | ||||
|           // TODO POST-as-GET
 | ||||
|           return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { | ||||
|             if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } | ||||
|             // https://github.com/certbot/certbot/issues/5721
 | ||||
|             var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); | ||||
|             //  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | ||||
|             var certs = { | ||||
|               expires: order.expires | ||||
|             , identifiers: order.identifiers | ||||
|             //, authorizations: order.authorizations
 | ||||
|             , cert: certsarr.shift() | ||||
|             //, privkey: privkeyPem
 | ||||
|             , chain: certsarr.join('\n') | ||||
|             }; | ||||
|             if (me.debug) { console.debug(certs); } | ||||
|             return certs; | ||||
|           }); | ||||
|         return ACME._finalizeOrder(me, options, validatedDomains); | ||||
|       }).then(function (order) { | ||||
|         if (me.debug) { console.debug('acme-v2: order was finalized'); } | ||||
|         // TODO POST-as-GET
 | ||||
|         return me.request({ method: 'GET', url: options._certificate, json: true }).then(function (resp) { | ||||
|           if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } | ||||
|           // https://github.com/certbot/certbot/issues/5721
 | ||||
|           var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); | ||||
|           //  cert, chain, fullchain, privkey, /*TODO, subject, altnames, issuedAt, expiresAt */
 | ||||
|           var certs = { | ||||
|             expires: order.expires | ||||
|           , identifiers: order.identifiers | ||||
|           //, authorizations: order.authorizations
 | ||||
|           , cert: certsarr.shift() | ||||
|           //, privkey: privkeyPem
 | ||||
|           , chain: certsarr.join('\n') | ||||
|           }; | ||||
|           if (me.debug) { console.debug(certs); } | ||||
|           return certs; | ||||
|         }); | ||||
|       }); | ||||
|     }); | ||||
| @ -847,9 +820,10 @@ ACME.create = function create(me) { | ||||
|   if (!me) { me = {}; } | ||||
|   // me.debug = true;
 | ||||
|   me.challengePrefixes = ACME.challengePrefixes; | ||||
|   me.RSA = me.RSA || require('rsa-compat').RSA; | ||||
|   me.Keypairs = me.Keypairs || me.RSA || require('rsa-compat').RSA; | ||||
|   me._nonces = []; | ||||
|   //me.Keypairs = me.Keypairs || require('keypairs');
 | ||||
|   me.request = me.request || require('@coolaj86/urequest'); | ||||
|   //me.request = me.request || require('@root/request');
 | ||||
|   if (!me.dig) { | ||||
|     me.dig = function (query) { | ||||
|       // TODO use digd.js
 | ||||
| @ -860,37 +834,33 @@ ACME.create = function create(me) { | ||||
| 
 | ||||
|           resolve({ | ||||
|             answer: records.map(function (rr) { | ||||
|               return { | ||||
|                 data: rr | ||||
|               }; | ||||
|               return { data: rr }; | ||||
|             }) | ||||
|           }); | ||||
|         }); | ||||
|       }); | ||||
|     }; | ||||
|   } | ||||
|   me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; | ||||
| 
 | ||||
| 
 | ||||
|   if ('function' !== typeof me._request) { | ||||
|     // MUST have a User-Agent string (see node.js version)
 | ||||
|     me._request = function (opts) { | ||||
|       return window.fetch(opts.url, opts).then(function (resp) { | ||||
|         return resp.json().then(function (json) { | ||||
|           var headers = {}; | ||||
|           Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); | ||||
|           return { headers: headers , body: json }; | ||||
|         }); | ||||
|       }); | ||||
|     }; | ||||
|   if ('function' !== typeof me.request) { | ||||
|     me.request = ACME._defaultRequest; | ||||
|   } | ||||
| 
 | ||||
|   me.init = function (_directoryUrl) { | ||||
|     me.directoryUrl = me.directoryUrl || _directoryUrl; | ||||
|   me.init = function (opts) { | ||||
|     function fin(dir) { | ||||
|       me._directoryUrls = dir; | ||||
|       me._tos = dir.meta.termsOfService; | ||||
|       return dir; | ||||
|     } | ||||
|     if (opts && opts.meta && opts.termsOfService) { | ||||
|       return Promise.resolve(fin(opts)); | ||||
|     } | ||||
|     if (!me.directoryUrl) { me.directoryUrl = opts; } | ||||
|     if ('string' !== typeof me.directoryUrl) { | ||||
|       throw new Error("you must supply either the ACME directory url as a string or an object of the ACME urls"); | ||||
|     } | ||||
|     return ACME._directory(me).then(function (resp) { | ||||
|       me._directoryUrls = resp.body; | ||||
|       me._tos = me._directoryUrls.meta.termsOfService; | ||||
|       return me._directoryUrls; | ||||
|       return fin(resp.body); | ||||
|     }); | ||||
|   }; | ||||
|   me.accounts = { | ||||
| @ -906,6 +876,84 @@ ACME.create = function create(me) { | ||||
|   return me; | ||||
| }; | ||||
| 
 | ||||
| // Handle nonce, signing, and request altogether
 | ||||
| ACME._jwsRequest = function (me, bigopts) { | ||||
|   return ACME._getNonce(me).then(function (nonce) { | ||||
|     bigopts.protected.nonce = nonce; | ||||
|     bigopts.protected.url = bigopts.url; | ||||
|     // protected.alg: added by Keypairs.signJws
 | ||||
|     return me.Keypairs.signJws( | ||||
|       { jwk: bigopts.options.accountKeypair.privateKeyJwk | ||||
|       , protected: bigopts.protected | ||||
|       , payload: bigopts.payload | ||||
|       } | ||||
|     ).then(function (jws) { | ||||
|       if (me.debug) { console.debug('[acme-v2] ' + bigopts.url + ':'); } | ||||
|       if (me.debug) { console.debug(jws); } | ||||
|       return ACME._request(me, { url: bigopts.url, json: jws }); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| // Handle some ACME-specific defaults
 | ||||
| ACME._request = function (me, opts) { | ||||
|   if (!opts.headers) { opts.headers = {}; } | ||||
|   if (opts.json && true !== opts.json) { | ||||
|     opts.headers['Content-Type'] = 'application/jose+json'; | ||||
|     opts.body = JSON.stringify(opts.json); | ||||
|     if (!opts.method) { opts.method = 'POST'; } | ||||
|   } | ||||
|   return me.request(opts).then(function (resp) { | ||||
|     resp = resp.toJSON(); | ||||
|     if (resp.headers['replay-nonce']) { | ||||
|       ACME._setNonce(me, resp.headers['replay-nonce']); | ||||
|     } | ||||
|     return resp; | ||||
|   }); | ||||
| }; | ||||
| // A very generic, swappable request lib
 | ||||
| ACME._defaultRequest = function (opts) { | ||||
|   // Note: normally we'd have to supply a User-Agent string, but not here in a browser
 | ||||
|   if (!opts.headers) { opts.headers = {}; } | ||||
|   if (opts.json) { | ||||
|     opts.headers.Accept = 'application/json'; | ||||
|     if (true !== opts.json) { opts.body = JSON.stringify(opts.json); } | ||||
|   } | ||||
|   if (!opts.method) { | ||||
|     opts.method = 'GET'; | ||||
|     if (opts.body) { opts.method = 'POST'; } | ||||
|   } | ||||
|   opts.cors = true; | ||||
|   return window.fetch(opts.url, opts).then(function (resp) { | ||||
|     var headers = {}; | ||||
|     var result = { statusCode: resp.status, headers: headers, toJSON: function () { return this; } }; | ||||
|     Array.from(resp.headers.entries()).forEach(function (h) { headers[h[0]] = h[1]; }); | ||||
|     if (!headers['content-type']) { | ||||
|       return result; | ||||
|     } | ||||
|     if (/json/.test(headers['content-type'])) { | ||||
|       return resp.json().then(function (json) { | ||||
|         result.body = json; | ||||
|         return result; | ||||
|       }); | ||||
|     } | ||||
|     return resp.text().then(function (txt) { | ||||
|       result.body = txt; | ||||
|       return result; | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| /* | ||||
| TODO | ||||
| Per-Order State Params | ||||
|       _kty | ||||
|       _alg | ||||
|       _finalize | ||||
|       _expires | ||||
|       _certificate | ||||
|       _order | ||||
|       _authorizations | ||||
| */ | ||||
| 
 | ||||
| ACME._toWebsafeBase64 = function (b64) { | ||||
|   return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); | ||||
| }; | ||||
|  | ||||
| @ -46,6 +46,8 @@ EC.generate = function (opts) { | ||||
|       "jwk" | ||||
|     , result.privateKey | ||||
|     ).then(function (privJwk) { | ||||
|       privJwk.key_ops = undefined; | ||||
|       privJwk.ext = undefined; | ||||
|       return { | ||||
|         private: privJwk | ||||
|       , public: EC.neuter({ jwk: privJwk }) | ||||
|  | ||||
							
								
								
									
										217
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										217
									
								
								lib/keypairs.js
									
									
									
									
									
								
							| @ -33,12 +33,20 @@ Keypairs.generate = function (opts) { | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| Keypairs.export = function (opts) { | ||||
|   return Eckles.export(opts).catch(function (err) { | ||||
|     return Rasha.export(opts).catch(function () { | ||||
|       return Promise.reject(err); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Chopping off the private parts is now part of the public API. | ||||
|  * I thought it sounded a little too crude at first, but it really is the best name in every possible way. | ||||
|  */ | ||||
| Keypairs.neuter = Keypairs._neuter = function (opts) { | ||||
| Keypairs.neuter = function (opts) { | ||||
|   /** trying to find the best balance of an immutable copy with custom attributes */ | ||||
|   var jwk = {}; | ||||
|   Object.keys(opts.jwk).forEach(function (k) { | ||||
| @ -63,7 +71,7 @@ Keypairs.thumbprint = function (opts) { | ||||
| Keypairs.publish = function (opts) { | ||||
|   if ('object' !== typeof opts.jwk || !opts.jwk.kty) { throw new Error("invalid jwk: " + JSON.stringify(opts.jwk)); } | ||||
| 
 | ||||
|   /** returns a copy */  | ||||
|   /** returns a copy */ | ||||
|   var jwk = Keypairs.neuter(opts); | ||||
| 
 | ||||
|   if (jwk.exp) { | ||||
| @ -128,11 +136,12 @@ Keypairs.signJws = function (opts) { | ||||
|       if (!opts.jwk) { | ||||
|         throw new Error("opts.jwk must exist and must declare 'typ'"); | ||||
|       } | ||||
|       return ('RSA' === opts.jwk.kty) ? "RS256" : "ES256"; | ||||
|       if (opts.jwk.alg) { return opts.jwk.alg; } | ||||
|       var typ = ('RSA' === opts.jwk.kty) ? "RS" : "ES"; | ||||
|       return typ + Keypairs._getBits(opts); | ||||
|     } | ||||
| 
 | ||||
|     function sign(pem) { | ||||
|       var header = opts.header; | ||||
|     function sign() { | ||||
|       var protect = opts.protected; | ||||
|       var payload = opts.payload; | ||||
| 
 | ||||
| @ -143,8 +152,9 @@ Keypairs.signJws = function (opts) { | ||||
|       if (false !== protect) { | ||||
|         if (!protect) { protect = {}; } | ||||
|         if (!protect.alg) { protect.alg = alg(); } | ||||
|         // There's a particular request where Let's Encrypt explicitly doesn't use a kid
 | ||||
|         if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } | ||||
|         // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
 | ||||
|         if (false === protect.kid) { protect.kid = undefined; } | ||||
|         else if (!protect.kid) { protect.kid = thumb; } | ||||
|         protectedHeader = JSON.stringify(protect); | ||||
|       } | ||||
| 
 | ||||
| @ -155,7 +165,7 @@ Keypairs.signJws = function (opts) { | ||||
|       // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
 | ||||
|       if (payload && ('string' !== typeof payload) | ||||
|         && ('undefined' === typeof payload.byteLength) | ||||
|         && ('undefined' === typeof payload.byteLength) | ||||
|         && ('undefined' === typeof payload.buffer) | ||||
|       ) { | ||||
|         payload = JSON.stringify(payload); | ||||
|       } | ||||
| @ -165,76 +175,147 @@ Keypairs.signJws = function (opts) { | ||||
|       } | ||||
| 
 | ||||
|       // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
 | ||||
|       var nodeAlg = "SHA" + (((protect||header).alg||'').replace(/^[^\d]+/, '')||'256'); | ||||
|       var protected64 = Enc.strToUrlBase64(protectedHeader); | ||||
|       var payload64 = Enc.bufToUrlBase64(payload); | ||||
|       var binsig = require('crypto') | ||||
|         .createSign(nodeAlg) | ||||
|         .update(protect ? (protected64 + "." + payload64) : payload64) | ||||
|         .sign(pem) | ||||
|       ; | ||||
|       if ('EC' === opts.jwk.kty) { | ||||
|         // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||
|         // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||
|         binsig = convertIfEcdsa(binsig); | ||||
|       } | ||||
|       var msg = protected64 + '.' + payload64; | ||||
| 
 | ||||
|       var sig = binsig.toString('base64') | ||||
|         .replace(/\+/g, '-') | ||||
|         .replace(/\//g, '_') | ||||
|         .replace(/=/g, '') | ||||
|       ; | ||||
|       return Keypairs._sign(opts, msg).then(function (buf) { | ||||
|         /* | ||||
|          * This will come back into play for CSRs, but not for JOSE | ||||
|         if ('EC' === opts.jwk.kty) { | ||||
|           // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||
|           // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||
|           binsig = convertIfEcdsa(binsig); | ||||
|         } | ||||
|         */ | ||||
|         var signedMsg = { | ||||
|           protected: protected64 | ||||
|         , payload: payload64 | ||||
|         , signature: Enc.bufToUrlBase64(buf) | ||||
|         }; | ||||
| 
 | ||||
|       return { | ||||
|         header: header | ||||
|       , protected: protected64 || undefined | ||||
|       , payload: payload64 | ||||
|       , signature: sig | ||||
|       }; | ||||
|         console.log('Signed Base64 Msg:'); | ||||
|         console.log(JSON.stringify(signedMsg, null, 2)); | ||||
| 
 | ||||
|         console.log('msg:', msg); | ||||
|         return signedMsg; | ||||
|       }); | ||||
|     } | ||||
| 
 | ||||
|     function convertIfEcdsa(binsig) { | ||||
|       // should have asn1 sequence header of 0x30
 | ||||
|       if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } | ||||
|       var index = 2; // first ecdsa "R" header byte
 | ||||
|       var len = binsig[1]; | ||||
|       var lenlen = 0; | ||||
|       // Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
 | ||||
|       if (0x80 & len) { | ||||
|         lenlen = len - 0x80; // should be exactly 1
 | ||||
|         len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
 | ||||
|         index += lenlen; | ||||
|       } | ||||
|       // should be of BigInt type
 | ||||
|       if (0x02 !== binsig[index]) { throw new Error("Impossible EC SHA R marker"); } | ||||
|       index += 1; | ||||
| 
 | ||||
|       var rlen = binsig[index]; | ||||
|       var bits = 32; | ||||
|       if (rlen > 49) { | ||||
|         bits = 64; | ||||
|       } else if (rlen > 33) { | ||||
|         bits = 48; | ||||
|       } | ||||
|       var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex'); | ||||
|       var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
 | ||||
|       var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex'); | ||||
|       if (2 *slen !== s.length) { throw new Error("Impossible EC SHA S length"); } | ||||
|       // There may be one byte of padding on either
 | ||||
|       while (r.length < 2*bits) { r = '00' + r; } | ||||
|       while (s.length < 2*bits) { s = '00' + s; } | ||||
|       if (2*(bits+1) === r.length) { r = r.slice(2); } | ||||
|       if (2*(bits+1) === s.length) { s = s.slice(2); } | ||||
|       return Enc.hexToBuf(r + s); | ||||
|     } | ||||
| 
 | ||||
|     if (opts.pem && opts.jwk) { | ||||
|       return sign(opts.pem); | ||||
|     if (opts.jwk) { | ||||
|       return sign(); | ||||
|     } else { | ||||
|       return Keypairs.export({ jwk: opts.jwk }).then(sign); | ||||
|       return Keypairs.import({ pem: opts.pem }).then(function (pair) { | ||||
|         opts.jwk = pair.private; | ||||
|         return sign(); | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| Keypairs._convertIfEcdsa = function (binsig) { | ||||
|   // should have asn1 sequence header of 0x30
 | ||||
|   if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } | ||||
|   var index = 2; // first ecdsa "R" header byte
 | ||||
|   var len = binsig[1]; | ||||
|   var lenlen = 0; | ||||
|   // Seek length of length if length is greater than 127 (i.e. two 512-bit / 64-byte R and S values)
 | ||||
|   if (0x80 & len) { | ||||
|     lenlen = len - 0x80; // should be exactly 1
 | ||||
|     len = binsig[2]; // should be <= 130 (two 64-bit SHA-512s, plus padding)
 | ||||
|     index += lenlen; | ||||
|   } | ||||
|   // should be of BigInt type
 | ||||
|   if (0x02 !== binsig[index]) { throw new Error("Impossible EC SHA R marker"); } | ||||
|   index += 1; | ||||
| 
 | ||||
|   var rlen = binsig[index]; | ||||
|   var bits = 32; | ||||
|   if (rlen > 49) { | ||||
|     bits = 64; | ||||
|   } else if (rlen > 33) { | ||||
|     bits = 48; | ||||
|   } | ||||
|   var r = binsig.slice(index + 1, index + 1 + rlen).toString('hex'); | ||||
|   var slen = binsig[index + 1 + rlen + 1]; // skip header and read length
 | ||||
|   var s = binsig.slice(index + 1 + rlen + 1 + 1).toString('hex'); | ||||
|   if (2 *slen !== s.length) { throw new Error("Impossible EC SHA S length"); } | ||||
|   // There may be one byte of padding on either
 | ||||
|   while (r.length < 2*bits) { r = '00' + r; } | ||||
|   while (s.length < 2*bits) { s = '00' + s; } | ||||
|   if (2*(bits+1) === r.length) { r = r.slice(2); } | ||||
|   if (2*(bits+1) === s.length) { s = s.slice(2); } | ||||
|   return Enc.hexToBuf(r + s); | ||||
| }; | ||||
| 
 | ||||
| Keypairs._sign = function (opts, payload) { | ||||
|   return Keypairs._import(opts).then(function (privkey) { | ||||
|     if ('string' === typeof payload) { | ||||
|       payload = (new TextEncoder()).encode(payload); | ||||
|     } | ||||
|     return window.crypto.subtle.sign( | ||||
|       { name: Keypairs._getName(opts) | ||||
|       , hash: { name: 'SHA-' + Keypairs._getBits(opts) } | ||||
|       } | ||||
|     , privkey | ||||
|     , payload | ||||
|     ).then(function (signature) { | ||||
|       // convert buffer to urlsafe base64
 | ||||
|       //return Enc.bufToUrlBase64(new Uint8Array(signature));
 | ||||
|       return new Uint8Array(signature); | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| Keypairs._getBits = function (opts) { | ||||
|   if (opts.alg) { return opts.alg.replace(/[a-z\-]/ig, ''); } | ||||
|   // base64 len to byte len
 | ||||
|   var len = Math.floor((opts.jwk.n||'').length * 0.75); | ||||
| 
 | ||||
|   // TODO this may be a bug
 | ||||
|   // need to confirm that the padding is no more or less than 1 byte
 | ||||
|   if (/521/.test(opts.jwk.crv) || len >= 511) { | ||||
|     return '512'; | ||||
|   } else if (/384/.test(opts.jwk.crv) || len >= 383) { | ||||
|     return '384'; | ||||
|   } | ||||
| 
 | ||||
|   return '256'; | ||||
| }; | ||||
| Keypairs._getName = function (opts) { | ||||
|   if (/EC/i.test(opts.jwk.kty)) { | ||||
|     return 'ECDSA'; | ||||
|   } else { | ||||
|     return 'RSASSA-PKCS1-v1_5'; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| Keypairs._import = function (opts) { | ||||
|   return Promise.resolve().then(function () { | ||||
|     var ops; | ||||
|     // all private keys just happen to have a 'd'
 | ||||
|     if (opts.jwk.d) { | ||||
|       ops = [ 'sign' ]; | ||||
|     } else { | ||||
|       ops = [ 'verify' ]; | ||||
|     } | ||||
|     // gotta mark it as extractable, as if it matters
 | ||||
|     opts.jwk.ext = true; | ||||
|     opts.jwk.key_ops = ops; | ||||
| 
 | ||||
|     console.log('jwk', opts.jwk); | ||||
|     return window.crypto.subtle.importKey( | ||||
|       "jwk" | ||||
|     , opts.jwk | ||||
|     , { name: Keypairs._getName(opts) | ||||
|       , namedCurve: opts.jwk.crv | ||||
|       , hash: { name: 'SHA-' + Keypairs._getBits(opts) } } | ||||
|     , true | ||||
|     , ops | ||||
|     ).then(function (privkey) { | ||||
|       delete opts.jwk.ext; | ||||
|       return privkey; | ||||
|     }); | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| function setTime(time) { | ||||
|   if ('number' === typeof time) { return time; } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user