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 Rasha = window.Rasha; | ||||||
|   var Eckles = window.Eckles; |   var Eckles = window.Eckles; | ||||||
|   var x509 = window.x509; |   var x509 = window.x509; | ||||||
|  |   var ACME = window.ACME; | ||||||
| 
 | 
 | ||||||
|   function $(sel) { |   function $(sel) { | ||||||
|     return document.querySelector(sel); |     return document.querySelector(sel); | ||||||
| @ -106,7 +107,22 @@ | |||||||
|       ev.preventDefault(); |       ev.preventDefault(); | ||||||
|       ev.stopPropagation(); |       ev.stopPropagation(); | ||||||
|       $('.js-loading').hidden = false; |       $('.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; |     $('.js-generate').hidden = false; | ||||||
|  | |||||||
							
								
								
									
										356
									
								
								lib/acme.js
									
									
									
									
									
								
							
							
						
						
									
										356
									
								
								lib/acme.js
									
									
									
									
									
								
							| @ -30,7 +30,7 @@ ACME.challengePrefixes = { | |||||||
| ACME.challengeTests = { | ACME.challengeTests = { | ||||||
|   'http-01': function (me, auth) { |   'http-01': function (me, auth) { | ||||||
|     var url = 'http://' + auth.hostname + ACME.challengePrefixes['http-01'] + '/' + auth.token; |     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; |       var err; | ||||||
| 
 | 
 | ||||||
|       // TODO limit the number of bytes that are allowed to be downloaded
 |       // TODO limit the number of bytes that are allowed to be downloaded
 | ||||||
| @ -76,16 +76,28 @@ ACME.challengeTests = { | |||||||
| 
 | 
 | ||||||
| ACME._directory = function (me) { | ACME._directory = function (me) { | ||||||
|   // GET-as-GET ok
 |   // 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) { | ACME._getNonce = function (me) { | ||||||
|   // GET-as-GET, HEAD-as-HEAD ok
 |   // GET-as-GET, HEAD-as-HEAD ok
 | ||||||
|   if (me._nonce) { return new Promise(function (resolve) { resolve(me._nonce); return; }); } |   var nonce; | ||||||
|   return me._request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |   while (true) { | ||||||
|     me._nonce = resp.toJSON().headers['replay-nonce']; |     nonce = me._nonces.shift(); | ||||||
|     return me._nonce; |     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
 | // ACME RFC Section 7.3 Account Creation
 | ||||||
| /* | /* | ||||||
|  { |  { | ||||||
| @ -109,7 +121,6 @@ ACME._getNonce = function (me) { | |||||||
| ACME._registerAccount = function (me, options) { | ACME._registerAccount = function (me, options) { | ||||||
|   if (me.debug) { console.debug('[acme-v2] accounts.create'); } |   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) { |     function agree(tosUrl) { | ||||||
| @ -124,11 +135,16 @@ ACME._registerAccount = function (me, options) { | |||||||
|       var jwk = options.accountKeypair.privateKeyJwk; |       var jwk = options.accountKeypair.privateKeyJwk; | ||||||
|       var p; |       var p; | ||||||
|       if (jwk) { |       if (jwk) { | ||||||
|           p = Promise.resolve({ private: jwk, public: Keypairs.neuter(jwk) }); |         // nix the browser jwk extras
 | ||||||
|  |         jwk.key_ops = undefined; | ||||||
|  |         jwk.ext = undefined; | ||||||
|  |         p = Promise.resolve({ private: jwk, public: Keypairs.neuter({ jwk: jwk }) }); | ||||||
|       } else { |       } else { | ||||||
|         p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); |         p = Keypairs.import({ pem: options.accountKeypair.privateKeyPem }); | ||||||
|       } |       } | ||||||
|       return p.then(function (pair) { |       return p.then(function (pair) { | ||||||
|  |         options.accountKeypair.privateKeyJwk = pair.private; | ||||||
|  |         options.accountKeypair.publicKeyJwk = pair.public; | ||||||
|         if (pair.public.kid) { |         if (pair.public.kid) { | ||||||
|           pair = JSON.parse(JSON.stringify(pair)); |           pair = JSON.parse(JSON.stringify(pair)); | ||||||
|           delete pair.public.kid; |           delete pair.public.kid; | ||||||
| @ -147,53 +163,44 @@ ACME._registerAccount = function (me, options) { | |||||||
|         , onlyReturnExisting: false |         , onlyReturnExisting: false | ||||||
|         , contact: contact |         , contact: contact | ||||||
|         }; |         }; | ||||||
|  |         var pExt; | ||||||
|         if (options.externalAccount) { |         if (options.externalAccount) { | ||||||
|             body.externalAccountBinding = me.RSA.signJws( |           pExt = me.Keypairs.signJws({ | ||||||
|             // TODO is HMAC the standard, or is this arbitrary?
 |             // TODO is HMAC the standard, or is this arbitrary?
 | ||||||
|               options.externalAccount.secret |             secret: options.externalAccount.secret | ||||||
|             , undefined |           , protected: { | ||||||
|             , { alg: options.externalAccount.alg || "HS256" |               alg: options.externalAccount.alg || "HS256" | ||||||
|             , kid: options.externalAccount.id |             , kid: options.externalAccount.id | ||||||
|             , url: me._directoryUrls.newAccount |             , url: me._directoryUrls.newAccount | ||||||
|             } |             } | ||||||
|             , Buffer.from(JSON.stringify(pair.public)) |           , 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); |           var payload = JSON.stringify(body); | ||||||
|           var jws = Keypairs.signJws( |           return ACME._jwsRequest(me, { | ||||||
|             options.accountKeypair |             options: options | ||||||
|           , undefined |  | ||||||
|           , { nonce: me._nonce |  | ||||||
|             , alg: (me._alg || 'RS256') |  | ||||||
|           , url: me._directoryUrls.newAccount |           , url: me._directoryUrls.newAccount | ||||||
|             , jwk: pair.public |           , protected: { kid: false, jwk: pair.public } | ||||||
|             } |           , payload: Enc.binToBuf(payload) | ||||||
|           , 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' |  | ||||||
|           , url: me._directoryUrls.newAccount |  | ||||||
|           , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|           , json: jws |  | ||||||
|           }).then(function (resp) { |           }).then(function (resp) { | ||||||
|             var account = resp.body; |             var account = resp.body; | ||||||
| 
 | 
 | ||||||
|             if (2 !== Math.floor(resp.statusCode / 100)) { |             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.headers.location; | ||||||
|             var location = resp.toJSON().headers.location; |  | ||||||
|             // the account id url
 |             // the account id url
 | ||||||
|             me._kid = location; |             options._kid = location; | ||||||
|             if (me.debug) { console.debug('[DEBUG] new account location:'); } |             if (me.debug) { console.debug('[DEBUG] new account location:'); } | ||||||
|             if (me.debug) { console.debug(location); } |             if (me.debug) { console.debug(location); } | ||||||
|             if (me.debug) { console.debug(resp.toJSON()); } |             if (me.debug) { console.debug(resp); } | ||||||
| 
 | 
 | ||||||
|             /* |             /* | ||||||
|             { |             { | ||||||
| @ -205,16 +212,17 @@ ACME._registerAccount = function (me, options) { | |||||||
|             if (!account) { account = { _emptyResponse: true, key: {} }; } |             if (!account) { account = { _emptyResponse: true, key: {} }; } | ||||||
|             // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 |             // https://git.coolaj86.com/coolaj86/acme-v2.js/issues/8
 | ||||||
|             if (!account.key) { account.key = {}; } |             if (!account.key) { account.key = {}; } | ||||||
|             account.key.kid = me._kid; |             account.key.kid = options._kid; | ||||||
|             return account; |             return account; | ||||||
|           }).then(resolve, reject); |           }).then(resolve, reject); | ||||||
|         }); |         }); | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } |     if (me.debug) { console.debug('[acme-v2] agreeToTerms'); } | ||||||
|     if (1 === options.agreeToTerms.length) { |     if (1 === options.agreeToTerms.length) { | ||||||
|       // newer promise API
 |       // newer promise API
 | ||||||
|         return options.agreeToTerms(me._tos).then(agree, reject); |       return Promise.resolve(options.agreeToTerms(me._tos)).then(agree, reject); | ||||||
|     } |     } | ||||||
|     else if (2 === options.agreeToTerms.length) { |     else if (2 === options.agreeToTerms.length) { | ||||||
|       // backwards compat cb API
 |       // backwards compat cb API
 | ||||||
| @ -228,7 +236,6 @@ ACME._registerAccount = function (me, options) { | |||||||
|         + ' Should be fn(tos) { return Promise<tos>; }')); |         + ' Should be fn(tos) { return Promise<tos>; }')); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| /* | /* | ||||||
|  POST /acme/new-order HTTP/1.1 |  POST /acme/new-order HTTP/1.1 | ||||||
| @ -250,10 +257,16 @@ ACME._registerAccount = function (me, options) { | |||||||
|    "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" |    "signature": "H6ZXtGjTZyUnPeKn...wEA4TklBdh3e454g" | ||||||
|  } |  } | ||||||
| */ | */ | ||||||
| ACME._getChallenges = function (me, options, auth) { | ACME._getChallenges = function (me, options, authUrl) { | ||||||
|   if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } |   if (me.debug) { console.debug('\n[DEBUG] getChallenges\n'); } | ||||||
|   // TODO POST-as-GET
 |   // 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; |     return resp.body; | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| @ -389,7 +402,8 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | |||||||
|   auth.hostname = auth.identifier.value; |   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
 |   // 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.altname = ACME._untame(auth.identifier.value, auth.wildcard); | ||||||
|   auth.thumbprint = me.RSA.thumbprint(options.accountKeypair); |   return me.Keypairs.thumbprint({ jwk: options.accountKeypair.publicKeyJwk }).then(function (thumb) { | ||||||
|  |     auth.thumbprint = thumb; | ||||||
|     //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 |     //   keyAuthorization = token || '.' || base64url(JWK_Thumbprint(accountKey))
 | ||||||
|     auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; |     auth.keyAuthorization = challenge.token + '.' + auth.thumbprint; | ||||||
|     // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 |     // conflicts with ACME challenge id url is already in use, so we call this challengeUrl instead
 | ||||||
| @ -400,6 +414,7 @@ ACME._challengeToAuth = function (me, options, request, challenge, dryrun) { | |||||||
|       auth.dnsAuthorization = hash; |       auth.dnsAuthorization = hash; | ||||||
|       return auth; |       return auth; | ||||||
|     }); |     }); | ||||||
|  |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| ACME._untame = function (name, wild) { | ACME._untame = function (name, wild) { | ||||||
| @ -436,25 +451,13 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
|    } |    } | ||||||
|    */ |    */ | ||||||
|   function deactivate() { |   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' |  | ||||||
|     , url: auth.url |  | ||||||
|     , headers: { 'Content-Type': 'application/jose+json' } |  | ||||||
|     , json: jws |  | ||||||
|     }).then(function (resp) { |  | ||||||
|     if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } |     if (me.debug) { console.debug('[acme-v2.js] deactivate:'); } | ||||||
|       if (me.debug) { console.debug(resp.headers); } |     return ACME._jwsRequest({ | ||||||
|       if (me.debug) { console.debug(resp.body); } |       options: options | ||||||
|       if (me.debug) { console.debug(); } |     , url: auth.url | ||||||
| 
 |     , protected: { kid: options._kid } | ||||||
|       me._nonce = resp.toJSON().headers['replay-nonce']; |     , payload: Enc.strToBuf(JSON.stringify({ "status": "deactivated" })) | ||||||
|  |     }).then(function (resp) { | ||||||
|       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } |       if (me.debug) { console.debug('deactivate challenge: resp.body:'); } | ||||||
|       if (me.debug) { console.debug(resp.body); } |       if (me.debug) { console.debug(resp.body); } | ||||||
|       return ACME._wait(DEAUTH_INTERVAL); |       return ACME._wait(DEAUTH_INTERVAL); | ||||||
| @ -472,7 +475,7 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
| 
 | 
 | ||||||
|     if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } |     if (me.debug) { console.debug('\n[DEBUG] statusChallenge\n'); } | ||||||
|     // TODO POST-as-GET
 |     // 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 ('processing' === resp.body.status) { | ||||||
|         if (me.debug) { console.debug('poll: again'); } |         if (me.debug) { console.debug('poll: again'); } | ||||||
|         return ACME._wait(RETRY_INTERVAL).then(pollStatus); |         return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||||
| @ -523,25 +526,13 @@ ACME._postChallenge = function (me, options, auth) { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function respondToChallenge() { |   function respondToChallenge() { | ||||||
|     var jws = me.RSA.signJws( |     if (me.debug) { console.debug('[acme-v2.js] responding to accept challenge:'); } | ||||||
|       options.accountKeypair |     return ACME._jwsRequest({ | ||||||
|     , undefined |       options: options | ||||||
|     , { 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' |  | ||||||
|     , url: auth.url |     , url: auth.url | ||||||
|     , headers: { 'Content-Type': 'application/jose+json' } |     , protected: { kid: options._kid } | ||||||
|     , json: jws |     , payload: Enc.strToBuf(JSON.stringify({})) | ||||||
|     }).then(function (resp) { |     }).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('respond to challenge: resp.body:'); } | ||||||
|       if (me.debug) { console.debug(resp.body); } |       if (me.debug) { console.debug(resp.body); } | ||||||
|       return ACME._wait(RETRY_INTERVAL).then(pollStatus); |       return ACME._wait(RETRY_INTERVAL).then(pollStatus); | ||||||
| @ -586,36 +577,26 @@ ACME._setChallenge = function (me, options, auth) { | |||||||
| }; | }; | ||||||
| ACME._finalizeOrder = function (me, options, validatedDomains) { | ACME._finalizeOrder = function (me, options, validatedDomains) { | ||||||
|   if (me.debug) { console.debug('finalizeOrder:'); } |   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 body = { csr: csr }; | ||||||
|   var payload = JSON.stringify(body); |   var payload = JSON.stringify(body); | ||||||
| 
 | 
 | ||||||
|   function pollCert() { |   function pollCert() { | ||||||
|     var jws = me.RSA.signJws( |     if (me.debug) { console.debug('[acme-v2.js] pollCert:'); } | ||||||
|       options.accountKeypair |     return ACME._jwsRequest({ | ||||||
|     , undefined |       options: options | ||||||
|     , { nonce: me._nonce, alg: (me._alg || 'RS256'), url: me._finalize, kid: me._kid } |     , url: options._finalize | ||||||
|     , Buffer.from(payload) |     , protected: { kid: options._kid } | ||||||
|     ); |     , payload: Enc.strToBuf(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 |  | ||||||
|     }).then(function (resp) { |     }).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('order finalized: resp.body:'); } | ||||||
|       if (me.debug) { console.debug(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) { |       if ('valid' === resp.body.status) { | ||||||
|         me._expires = resp.body.expires; |         options._expires = resp.body.expires; | ||||||
|         me._certificate = resp.body.certificate; |         options._certificate = resp.body.certificate; | ||||||
| 
 | 
 | ||||||
|         return resp.body; // return order
 |         return resp.body; // return order
 | ||||||
|       } |       } | ||||||
| @ -672,6 +653,11 @@ ACME._finalizeOrder = function (me, options, validatedDomains) { | |||||||
| 
 | 
 | ||||||
|   return pollCert(); |   return pollCert(); | ||||||
| }; | }; | ||||||
|  | // _kid
 | ||||||
|  | // registerAccount
 | ||||||
|  | // postChallenge
 | ||||||
|  | // finalizeOrder
 | ||||||
|  | // getCertificate
 | ||||||
| ACME._getCertificate = function (me, options) { | ACME._getCertificate = function (me, options) { | ||||||
|   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } |   if (me.debug) { console.debug('[acme-v2] DEBUG get cert 1'); } | ||||||
| 
 | 
 | ||||||
| @ -704,9 +690,8 @@ 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
 |   // 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) { |   if (options.accountKid || options.account && options.account.kid) { | ||||||
|       me._kid = options.accountKid || options.account.kid; |     options._kid = options.accountKid || options.account.kid; | ||||||
|   } else { |   } else { | ||||||
|     //return Promise.reject(new Error("must include KeyID"));
 |     //return Promise.reject(new Error("must include KeyID"));
 | ||||||
|     // This is an idempotent request. It'll return the same account for the same public key.
 |     // This is an idempotent request. It'll return the same account for the same public key.
 | ||||||
| @ -715,12 +700,10 @@ ACME._getCertificate = function (me, options) { | |||||||
|       return ACME._getCertificate(me, options); |       return ACME._getCertificate(me, options); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
| 
 | 
 | ||||||
|   // Do a little dry-run / self-test
 |   // Do a little dry-run / self-test
 | ||||||
|   return ACME._testChallenges(me, options).then(function () { |   return ACME._testChallenges(me, options).then(function () { | ||||||
|     if (me.debug) { console.debug('[acme-v2] certificates.create'); } |     if (me.debug) { console.debug('[acme-v2] certificates.create'); } | ||||||
|     return ACME._getNonce(me).then(function () { |  | ||||||
|     var body = { |     var body = { | ||||||
|       // raw wildcard syntax MUST be used here
 |       // raw wildcard syntax MUST be used here
 | ||||||
|       identifiers: options.domains.sort(function (a, b) { |       identifiers: options.domains.sort(function (a, b) { | ||||||
| @ -738,42 +721,33 @@ ACME._getCertificate = function (me, options) { | |||||||
| 
 | 
 | ||||||
|     var payload = JSON.stringify(body); |     var payload = JSON.stringify(body); | ||||||
|     // determine the signing algorithm to use in protected header // TODO isn't that handled by the signer?
 |     // 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'); |     options._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)
 |     options._alg = ('EC' === options._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') |  | ||||||
|       ); |  | ||||||
| 
 |  | ||||||
|     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } |     if (me.debug) { console.debug('\n[DEBUG] newOrder\n'); } | ||||||
|       me._nonce = null; |     return ACME._jwsRequest({ | ||||||
|       return me._request({ |       options: options | ||||||
|         method: 'POST' |  | ||||||
|     , url: me._directoryUrls.newOrder |     , url: me._directoryUrls.newOrder | ||||||
|       , headers: { 'Content-Type': 'application/jose+json' } |     , protected: { kid: options._kid } | ||||||
|       , json: jws |     , payload: Enc.strToBuf(payload) | ||||||
|     }).then(function (resp) { |     }).then(function (resp) { | ||||||
|         me._nonce = resp.toJSON().headers['replay-nonce']; |       var location = resp.headers.location; | ||||||
|         var location = resp.toJSON().headers.location; |  | ||||||
|       var setAuths; |       var setAuths; | ||||||
|       var auths = []; |       var auths = []; | ||||||
|         if (me.debug) { console.debug(location); } // the account id url
 |       if (me.debug) { console.debug('[ordered]', location); } // the account id url
 | ||||||
|         if (me.debug) { console.debug(resp.toJSON()); } |       if (me.debug) { console.debug(resp); } | ||||||
|         me._authorizations = resp.body.authorizations; |       options._authorizations = resp.body.authorizations; | ||||||
|         me._order = location; |       options._order = location; | ||||||
|         me._finalize = resp.body.finalize; |       options._finalize = resp.body.finalize; | ||||||
|         //if (me.debug) console.debug('[DEBUG] finalize:', me._finalize); return;
 |       //if (me.debug) console.debug('[DEBUG] finalize:', options._finalize); return;
 | ||||||
| 
 | 
 | ||||||
|         if (!me._authorizations) { |       if (!options._authorizations) { | ||||||
|         return Promise.reject(new Error( |         return Promise.reject(new Error( | ||||||
|           "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" |           "[acme-v2.js] authorizations were not fetched for '" + options.domains.join() + "':\n" | ||||||
|           + JSON.stringify(resp.body) |           + JSON.stringify(resp.body) | ||||||
|         )); |         )); | ||||||
|       } |       } | ||||||
|       if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } |       if (me.debug) { console.debug("[acme-v2] POST newOrder has authorizations"); } | ||||||
|         setAuths = me._authorizations.slice(0); |       setAuths = options._authorizations.slice(0); | ||||||
| 
 | 
 | ||||||
|       function setNext() { |       function setNext() { | ||||||
|         var authUrl = setAuths.shift(); |         var authUrl = setAuths.shift(); | ||||||
| @ -821,7 +795,7 @@ ACME._getCertificate = function (me, options) { | |||||||
|       }).then(function (order) { |       }).then(function (order) { | ||||||
|         if (me.debug) { console.debug('acme-v2: order was finalized'); } |         if (me.debug) { console.debug('acme-v2: order was finalized'); } | ||||||
|         // TODO POST-as-GET
 |         // TODO POST-as-GET
 | ||||||
|           return me._request({ method: 'GET', url: me._certificate, json: true }).then(function (resp) { |         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:'); } |           if (me.debug) { console.debug('acme-v2: csr submitted and cert received:'); } | ||||||
|           // https://github.com/certbot/certbot/issues/5721
 |           // https://github.com/certbot/certbot/issues/5721
 | ||||||
|           var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); |           var certsarr = ACME.splitPemChain(ACME.formatPemChain((resp.body||''))); | ||||||
| @ -840,16 +814,16 @@ ACME._getCertificate = function (me, options) { | |||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
|   }); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| ACME.create = function create(me) { | ACME.create = function create(me) { | ||||||
|   if (!me) { me = {}; } |   if (!me) { me = {}; } | ||||||
|   // me.debug = true;
 |   // me.debug = true;
 | ||||||
|   me.challengePrefixes = ACME.challengePrefixes; |   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.Keypairs = me.Keypairs || require('keypairs');
 | ||||||
|   me.request = me.request || require('@coolaj86/urequest'); |   //me.request = me.request || require('@root/request');
 | ||||||
|   if (!me.dig) { |   if (!me.dig) { | ||||||
|     me.dig = function (query) { |     me.dig = function (query) { | ||||||
|       // TODO use digd.js
 |       // TODO use digd.js
 | ||||||
| @ -860,37 +834,33 @@ ACME.create = function create(me) { | |||||||
| 
 | 
 | ||||||
|           resolve({ |           resolve({ | ||||||
|             answer: records.map(function (rr) { |             answer: records.map(function (rr) { | ||||||
|               return { |               return { data: rr }; | ||||||
|                 data: rr |  | ||||||
|               }; |  | ||||||
|             }) |             }) | ||||||
|           }); |           }); | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|   me.promisify = me.promisify || require('util').promisify /*node v8+*/ || require('bluebird').promisify /*node v6*/; |  | ||||||
| 
 | 
 | ||||||
| 
 |   if ('function' !== typeof me.request) { | ||||||
|   if ('function' !== typeof me._request) { |     me.request = ACME._defaultRequest; | ||||||
|     // 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 }; |  | ||||||
|         }); |  | ||||||
|       }); |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   me.init = function (_directoryUrl) { |   me.init = function (opts) { | ||||||
|     me.directoryUrl = me.directoryUrl || _directoryUrl; |     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) { |     return ACME._directory(me).then(function (resp) { | ||||||
|       me._directoryUrls = resp.body; |       return fin(resp.body); | ||||||
|       me._tos = me._directoryUrls.meta.termsOfService; |  | ||||||
|       return me._directoryUrls; |  | ||||||
|     }); |     }); | ||||||
|   }; |   }; | ||||||
|   me.accounts = { |   me.accounts = { | ||||||
| @ -906,6 +876,84 @@ ACME.create = function create(me) { | |||||||
|   return 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) { | ACME._toWebsafeBase64 = function (b64) { | ||||||
|   return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); |   return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g,""); | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -46,6 +46,8 @@ EC.generate = function (opts) { | |||||||
|       "jwk" |       "jwk" | ||||||
|     , result.privateKey |     , result.privateKey | ||||||
|     ).then(function (privJwk) { |     ).then(function (privJwk) { | ||||||
|  |       privJwk.key_ops = undefined; | ||||||
|  |       privJwk.ext = undefined; | ||||||
|       return { |       return { | ||||||
|         private: privJwk |         private: privJwk | ||||||
|       , public: EC.neuter({ jwk: privJwk }) |       , public: EC.neuter({ jwk: privJwk }) | ||||||
|  | |||||||
							
								
								
									
										137
									
								
								lib/keypairs.js
									
									
									
									
									
								
							
							
						
						
									
										137
									
								
								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. |  * 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. |  * 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 */ |   /** trying to find the best balance of an immutable copy with custom attributes */ | ||||||
|   var jwk = {}; |   var jwk = {}; | ||||||
|   Object.keys(opts.jwk).forEach(function (k) { |   Object.keys(opts.jwk).forEach(function (k) { | ||||||
| @ -128,11 +136,12 @@ Keypairs.signJws = function (opts) { | |||||||
|       if (!opts.jwk) { |       if (!opts.jwk) { | ||||||
|         throw new Error("opts.jwk must exist and must declare 'typ'"); |         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) { |     function sign() { | ||||||
|       var header = opts.header; |  | ||||||
|       var protect = opts.protected; |       var protect = opts.protected; | ||||||
|       var payload = opts.payload; |       var payload = opts.payload; | ||||||
| 
 | 
 | ||||||
| @ -143,8 +152,9 @@ Keypairs.signJws = function (opts) { | |||||||
|       if (false !== protect) { |       if (false !== protect) { | ||||||
|         if (!protect) { protect = {}; } |         if (!protect) { protect = {}; } | ||||||
|         if (!protect.alg) { protect.alg = alg(); } |         if (!protect.alg) { protect.alg = alg(); } | ||||||
|         // There's a particular request where Let's Encrypt explicitly doesn't use a kid
 |         // There's a particular request where ACME / Let's Encrypt explicitly doesn't use a kid
 | ||||||
|         if (!protect.kid && false !== protect.kid) { protect.kid = thumb; } |         if (false === protect.kid) { protect.kid = undefined; } | ||||||
|  |         else if (!protect.kid) { protect.kid = thumb; } | ||||||
|         protectedHeader = JSON.stringify(protect); |         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)
 |       // Trying to detect if it's a plain object (not Buffer, ArrayBuffer, Array, Uint8Array, etc)
 | ||||||
|       if (payload && ('string' !== typeof payload) |       if (payload && ('string' !== typeof payload) | ||||||
|         && ('undefined' === typeof payload.byteLength) |         && ('undefined' === typeof payload.byteLength) | ||||||
|         && ('undefined' === typeof payload.byteLength) |         && ('undefined' === typeof payload.buffer) | ||||||
|       ) { |       ) { | ||||||
|         payload = JSON.stringify(payload); |         payload = JSON.stringify(payload); | ||||||
|       } |       } | ||||||
| @ -165,35 +175,44 @@ Keypairs.signJws = function (opts) { | |||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       // node specifies RSA-SHAxxx even when it's actually ecdsa (it's all encoded x509 shasums anyway)
 |       // 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 protected64 = Enc.strToUrlBase64(protectedHeader); | ||||||
|       var payload64 = Enc.bufToUrlBase64(payload); |       var payload64 = Enc.bufToUrlBase64(payload); | ||||||
|       var binsig = require('crypto') |       var msg = protected64 + '.' + payload64; | ||||||
|         .createSign(nodeAlg) | 
 | ||||||
|         .update(protect ? (protected64 + "." + payload64) : payload64) |       return Keypairs._sign(opts, msg).then(function (buf) { | ||||||
|         .sign(pem) |         /* | ||||||
|       ; |          * This will come back into play for CSRs, but not for JOSE | ||||||
|         if ('EC' === opts.jwk.kty) { |         if ('EC' === opts.jwk.kty) { | ||||||
|           // ECDSA JWT signatures differ from "normal" ECDSA signatures
 |           // ECDSA JWT signatures differ from "normal" ECDSA signatures
 | ||||||
|           // https://tools.ietf.org/html/rfc7518#section-3.4
 |           // https://tools.ietf.org/html/rfc7518#section-3.4
 | ||||||
|           binsig = convertIfEcdsa(binsig); |           binsig = convertIfEcdsa(binsig); | ||||||
|         } |         } | ||||||
| 
 |         */ | ||||||
|       var sig = binsig.toString('base64') |         var signedMsg = { | ||||||
|         .replace(/\+/g, '-') |           protected: protected64 | ||||||
|         .replace(/\//g, '_') |  | ||||||
|         .replace(/=/g, '') |  | ||||||
|       ; |  | ||||||
| 
 |  | ||||||
|       return { |  | ||||||
|         header: header |  | ||||||
|       , protected: protected64 || undefined |  | ||||||
|         , payload: payload64 |         , payload: payload64 | ||||||
|       , signature: sig |         , signature: Enc.bufToUrlBase64(buf) | ||||||
|         }; |         }; | ||||||
|  | 
 | ||||||
|  |         console.log('Signed Base64 Msg:'); | ||||||
|  |         console.log(JSON.stringify(signedMsg, null, 2)); | ||||||
|  | 
 | ||||||
|  |         console.log('msg:', msg); | ||||||
|  |         return signedMsg; | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     function convertIfEcdsa(binsig) { |     if (opts.jwk) { | ||||||
|  |       return sign(); | ||||||
|  |     } else { | ||||||
|  |       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
 |   // should have asn1 sequence header of 0x30
 | ||||||
|   if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } |   if (0x30 !== binsig[0]) { throw new Error("Impossible EC SHA head marker"); } | ||||||
|   var index = 2; // first ecdsa "R" header byte
 |   var index = 2; // first ecdsa "R" header byte
 | ||||||
| @ -226,13 +245,75 @@ Keypairs.signJws = function (opts) { | |||||||
|   if (2*(bits+1) === r.length) { r = r.slice(2); } |   if (2*(bits+1) === r.length) { r = r.slice(2); } | ||||||
|   if (2*(bits+1) === s.length) { s = s.slice(2); } |   if (2*(bits+1) === s.length) { s = s.slice(2); } | ||||||
|   return Enc.hexToBuf(r + s); |   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'; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|     if (opts.pem && opts.jwk) { |   return '256'; | ||||||
|       return sign(opts.pem); | }; | ||||||
|  | Keypairs._getName = function (opts) { | ||||||
|  |   if (/EC/i.test(opts.jwk.kty)) { | ||||||
|  |     return 'ECDSA'; | ||||||
|   } else { |   } else { | ||||||
|       return Keypairs.export({ jwk: opts.jwk }).then(sign); |     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; | ||||||
|  |     }); | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user