initial commit
This commit is contained in:
commit
c3df955564
|
@ -0,0 +1,15 @@
|
|||
0.0.3-beta / 2015-12-13
|
||||
=======================
|
||||
|
||||
* Added PKCS#12 support
|
||||
* Added some basic tests
|
||||
|
||||
0.0.2-beta / 2015-12-09
|
||||
=======================
|
||||
|
||||
* Code improvements (error handling)
|
||||
|
||||
0.0.1-alpha / 2015-12-08
|
||||
========================
|
||||
|
||||
* Initial release
|
|
@ -0,0 +1,373 @@
|
|||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
|
@ -0,0 +1,94 @@
|
|||
# letiny
|
||||
Tiny acme client library and CLI to obtain ssl certificates (without using external commands like openssl).
|
||||
|
||||
|
||||
## Usage:
|
||||
`npm install letiny`
|
||||
|
||||
|
||||
### Using the "webroot" option
|
||||
This will create a file in `/var/www/example.com/.well-known/acme-challenge/` to verify the domain.
|
||||
```js
|
||||
require('letiny').getCert({
|
||||
email:'me@example.com',
|
||||
domains:['example.com', 'www.example.com'],
|
||||
webroot:'/var/www/example.com',
|
||||
certFile:'./cert.pem',
|
||||
keyFile:'./key.pem',
|
||||
caFile:'./ca.pem',
|
||||
agreeTerms:true
|
||||
}, function(err, cert, key, cacert) {
|
||||
console.log(err || cert+'\n'+key+'\n'+cacert);
|
||||
});
|
||||
```
|
||||
|
||||
### Using the "challenge" option
|
||||
This allows you to provide the challenge data on your own, so you can obtain certificates on-the-fly within your software.
|
||||
```js
|
||||
require('letiny').getCert({
|
||||
email:'me@example.com',
|
||||
domains:'example.com',
|
||||
challenge:function(domain, path, data, done) {
|
||||
// make http://+domain+path serving "data"
|
||||
done();
|
||||
},
|
||||
certFile:'./cert.pem',
|
||||
keyFile:'./key.pem',
|
||||
caFile:'./ca.pem',
|
||||
agreeTerms:true
|
||||
}, function(err, cert, key, cacert) {
|
||||
console.log(err || cert+'\n'+key+'\n'+cacert);
|
||||
});
|
||||
```
|
||||
|
||||
### Options
|
||||
#### Required:
|
||||
* `email`: Your email adress
|
||||
* `domains`: Comma seperated string or array
|
||||
* `agreeTerms`: You need to agree the terms
|
||||
* `webroot` (string) or `challenge` (function)
|
||||
|
||||
#### Optional:
|
||||
* `certFile`: Path to save certificate
|
||||
* `keyFile`: Path to save private key
|
||||
* `caFile`: Path to save issuer certificate
|
||||
* `pfxFile`: Path to save PKCS#12 certificate
|
||||
* `pfxPassword`: Password for PKCS#12 certificate
|
||||
* `aes`: (boolean), use AES instead of 3DES for PKCS#12 certificate
|
||||
* `newReg`: URL, use *https://acme-staging.api.letsencrypt.org/acme/new-reg* for testing
|
||||
|
||||
|
||||
## Command line interface
|
||||
```sudo npm install letiny -g```
|
||||
#### Options:
|
||||
```
|
||||
-h, --help output usage information
|
||||
-e, --email <email> your email address
|
||||
-w, --webroot <path> path for webroot verification
|
||||
-m, --manual use manual verification
|
||||
-d, --domains <domains> domains (comma seperated)
|
||||
-c, --cert <path> path to save your certificate (cert.pem)
|
||||
-k, --key <path> path to save your private key (privkey.pem)
|
||||
-i, --ca <path> path to save issuer certificate (cacert.pem)
|
||||
--pfx <path> path to save PKCS#12 certificate (optional)
|
||||
--password <password> password for PKCS#12 certificate (optional)
|
||||
--aes use AES instead of 3DES for PKCS#12
|
||||
--agree agree terms of the ACME CA (required)
|
||||
--newreg <URL> optional AMCE server newReg URL
|
||||
--debug print debug information
|
||||
```
|
||||
When --pfx is used without --cert, --key and --ca no .pem files will be created.
|
||||
|
||||
#### Examples:
|
||||
```
|
||||
letiny -e me@example.com -w /var/www/example.com -d example.com --agree
|
||||
letiny -e me@example.com -m -d example.com -c cert.pem -k key.pem -i ca.pem --agree
|
||||
letiny -e me@example.com -m -d example.com,www.example.com --agree
|
||||
letiny -e me@example.com -m -d example.com --pfx cert.pfx --password secret --agree
|
||||
letiny --email me@example.com --webroot ./ --domains example.com --agree
|
||||
```
|
||||
|
||||
|
||||
## Licence
|
||||
MPL 2.0
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright 2014 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
module.exports = {
|
||||
|
||||
fromStandardB64: function(x) {
|
||||
return x.replace(/[+]/g, "-").replace(/\//g, "_").replace(/=/g,"");
|
||||
},
|
||||
|
||||
toStandardB64: function(x) {
|
||||
var b64 = x.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
||||
|
||||
switch (b64.length % 4) {
|
||||
case 2: b64 += "=="; break;
|
||||
case 3: b64 += "="; break;
|
||||
}
|
||||
|
||||
return b64;
|
||||
},
|
||||
|
||||
b64enc: function(buffer) {
|
||||
return this.fromStandardB64(buffer.toString("base64"));
|
||||
},
|
||||
|
||||
b64dec: function(str) {
|
||||
return new Buffer(this.toStandardB64(str), "base64");
|
||||
},
|
||||
|
||||
isB64String: function(x) {
|
||||
return (typeof(x) == "string") && !x.match(/[^a-zA-Z0-9_-]/);
|
||||
},
|
||||
|
||||
fieldsPresent: function(fields, object) {
|
||||
for (var i in fields) {
|
||||
if (!(fields[i] in object)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validSignature: function(sig) {
|
||||
return ((typeof(sig) == "object") &&
|
||||
("alg" in sig) && (typeof(sig.alg) == "string") &&
|
||||
("nonce" in sig) && this.isB64String(sig.nonce) &&
|
||||
("sig" in sig) && this.isB64String(sig.sig) &&
|
||||
("jwk" in sig) && this.validJWK(sig.jwk));
|
||||
},
|
||||
|
||||
validJWK: function(jwk) {
|
||||
return ((typeof(jwk) == "object") && ("kty" in jwk) && (
|
||||
((jwk.kty == "RSA")
|
||||
&& ("n" in jwk) && this.isB64String(jwk.n)
|
||||
&& ("e" in jwk) && this.isB64String(jwk.e)) ||
|
||||
((jwk.kty == "EC")
|
||||
&& ("crv" in jwk)
|
||||
&& ("x" in jwk) && this.isB64String(jwk.x)
|
||||
&& ("y" in jwk) && this.isB64String(jwk.y))
|
||||
) && !("d" in jwk));
|
||||
},
|
||||
|
||||
// A simple, non-standard fingerprint for a JWK,
|
||||
// just so that we don't have to store objects
|
||||
keyFingerprint: function(jwk) {
|
||||
switch (jwk.kty) {
|
||||
case "RSA": return jwk.n;
|
||||
case "EC": return jwk.crv + jwk.x + jwk.y;
|
||||
}
|
||||
throw "Unrecognized key type";
|
||||
}
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/local/bin/node
|
||||
var app=require('commander'), letiny=require('./client'), examples=[
|
||||
'letiny -e me@example.com -w /var/www/example.com -d example.com --agree',
|
||||
'letiny -e me@example.com -m -d example.com -c cert.pem -k key.pem -i ca.pem --agree',
|
||||
'letiny -e me@example.com -m -d example.com,www.example.com --agree',
|
||||
'letiny --email me@example.com --webroot ./ --domains example.com --agree'
|
||||
];
|
||||
|
||||
app
|
||||
.option('-e, --email <email>', 'your email address')
|
||||
.option('-w, --webroot <path>', 'path for webroot verification OR')
|
||||
.option('-m, --manual', 'use manual verification')
|
||||
.option('-d, --domains <domains>', 'domains (comma seperated)')
|
||||
.option('-c, --cert <path>', 'path to save your certificate (cert.pem)')
|
||||
.option('-k, --key <path>', 'path to save your private key (privkey.pem)')
|
||||
.option('-i, --ca <path>', 'path to save issuer certificate (cacert.pem)')
|
||||
.option('--pfx <path>', 'path to save PKCS#12 certificate (optional)')
|
||||
.option('--password <password>', 'password for PKCS#12 certificate (optional)')
|
||||
.option('--aes', 'use AES instead of 3DES for PKCS#12')
|
||||
.option('--agree', 'agree terms of the ACME CA (required)')
|
||||
.option('--newreg <URL>', 'optional AMCE server newReg URL')
|
||||
.option('--debug', 'print debug information')
|
||||
.on('--help', function() {
|
||||
console.log(' Examples:\n\n '+examples.join('\n ')+'\n');
|
||||
})
|
||||
.parse(process.argv);
|
||||
|
||||
if (app.rawArgs.length<=2) {
|
||||
return app.parse(['', '', '-h']);
|
||||
} else if (!app.webroot && !app.manual) {
|
||||
return console.log('Error: You need to use "--manual" or "--webroot <path>"');
|
||||
} else if (!app.domains) {
|
||||
return console.log('Error: You need to specify "--domains <domain>"');
|
||||
} else if (!app.email) {
|
||||
return console.log('Error: You need to specify your "--email <address>"');
|
||||
} else if (!app.agree) {
|
||||
return console.log('Error: You need to "--agree" the terms');
|
||||
}
|
||||
|
||||
console.log('Generating keys and requesting certificate...');
|
||||
|
||||
letiny.getCert({
|
||||
email:app.email,
|
||||
domains:app.domains,
|
||||
webroot:app.webroot,
|
||||
challenge:manualVerification,
|
||||
certFile:app.cert || (app.pfx ? false : 'cert.pem'),
|
||||
keyFile:app.key || (app.pfx ? false : 'privkey.pem'),
|
||||
caFile:app.ca || (app.pfx ? false : 'cacert.pem'),
|
||||
pfxFile:app.pfx,
|
||||
pfxPassword:app.password,
|
||||
aes:app.aes,
|
||||
newReg:app.newreg,
|
||||
agreeTerms:app.agree,
|
||||
debug:app.debug
|
||||
}, function(err, cert, key, cacert) {
|
||||
if (!err && cert && key && cacert) {
|
||||
console.log('Files successfully saved.');
|
||||
process.exit(0);
|
||||
}
|
||||
console.error('Error: ', err.stack || err || 'Something went wrong...');
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
function manualVerification(domain, path, data, done) {
|
||||
var rl=require('readline').createInterface({
|
||||
input:process.stdin,
|
||||
output:process.stdout
|
||||
});
|
||||
console.log('\nCreate this file: http://'+domain+path);
|
||||
console.log(' containing this: '+data+'\n');
|
||||
rl.question('Press ENTER when done or Ctrl+C to exit\n', function() {
|
||||
rl.close();
|
||||
done();
|
||||
});
|
||||
}
|
|
@ -0,0 +1,463 @@
|
|||
/*!
|
||||
* letiny
|
||||
* Copyright(c) 2015 Anatol Sommer <anatol@anatol.at>
|
||||
* Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
|
||||
* MPL 2.0
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var _DEBUG, NOOP=new Function(), log=NOOP,
|
||||
mkdirp=require('mkdirp').sync, request=require('request'),
|
||||
forge=require('node-forge'), pki=forge.pki,
|
||||
cryptoUtil=require('./crypto-util'), util=require('./acme-util'),
|
||||
fs=require('fs'), path=require('path');
|
||||
|
||||
function Acme(privateKey) {
|
||||
this.privateKey=privateKey;
|
||||
this.nonces=[];
|
||||
}
|
||||
|
||||
Acme.prototype.getNonce=function(url, cb) {
|
||||
var self=this;
|
||||
|
||||
request.head({
|
||||
url:url,
|
||||
}, function(err, res, body) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (res && 'replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
cb(new Error('Failed to get nonce for request'));
|
||||
});
|
||||
};
|
||||
|
||||
Acme.prototype.post=function(url, body, cb) {
|
||||
var self=this, payload, jws, signed;
|
||||
|
||||
if (this.nonces.length===0) {
|
||||
this.getNonce(url, function(err) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
self.post(url, body, cb);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
log('Using nonce: '+this.nonces[0]);
|
||||
payload=JSON.stringify(body, null, 2);
|
||||
jws=cryptoUtil.generateSignature(
|
||||
this.privateKey, new Buffer(payload), this.nonces.shift()
|
||||
);
|
||||
signed=JSON.stringify(jws, null, 2);
|
||||
|
||||
log('Posting to '+url);
|
||||
log(signed.green);
|
||||
log('Payload:'+payload.blue);
|
||||
|
||||
return request.post({
|
||||
url:url,
|
||||
body:signed,
|
||||
encoding:null
|
||||
}, function(err, res, body) {
|
||||
var parsed;
|
||||
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return cb(err);
|
||||
}
|
||||
if (res) {
|
||||
log(('HTTP/1.1 '+res.statusCode).yellow);
|
||||
}
|
||||
|
||||
Object.keys(res.headers).forEach(function(key) {
|
||||
var value, upcased;
|
||||
value=res.headers[key];
|
||||
upcased=key.charAt(0).toUpperCase()+key.slice(1);
|
||||
log((upcased+': '+value).yellow);
|
||||
});
|
||||
|
||||
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
|
||||
try {
|
||||
parsed=JSON.parse(body);
|
||||
log(JSON.stringify(parsed, null, 2).cyan);
|
||||
} catch(err) {
|
||||
log(body.toString().cyan);
|
||||
}
|
||||
}
|
||||
|
||||
if ('replay-nonce' in res.headers) {
|
||||
log('Storing nonce: '+res.headers['replay-nonce']);
|
||||
self.nonces.push(res.headers['replay-nonce']);
|
||||
}
|
||||
|
||||
cb(err, res, body);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function getCert(options, cb) {
|
||||
var state={
|
||||
validatedDomains:[],
|
||||
validAuthorizationURLs:[]
|
||||
};
|
||||
|
||||
options.newReg=options.newReg || 'https://acme-v01.api.letsencrypt.org/acme/new-reg';
|
||||
|
||||
if (!options.email) {
|
||||
return cb(new Error('No "email" option given!'));
|
||||
}
|
||||
if (typeof options.domains==='string') {
|
||||
state.domains=options.domains.split(/[, ]+/);
|
||||
} else if (options.domains && options.domains instanceof Array) {
|
||||
state.domains=options.domains.slice();
|
||||
} else {
|
||||
return cb(new Error('No valid "domains" option given!'));
|
||||
}
|
||||
|
||||
if ((_DEBUG=options.debug)) {
|
||||
if (!''.green) {
|
||||
require('colors');
|
||||
}
|
||||
log=console.log.bind(console);
|
||||
} else {
|
||||
log=NOOP;
|
||||
}
|
||||
|
||||
makeAccountKeyPair();
|
||||
|
||||
function makeAccountKeyPair() {
|
||||
var keypair;
|
||||
log('Generating account keypair...');
|
||||
keypair=pki.rsa.generateKeyPair(2048);
|
||||
state.accountKeyPair=cryptoUtil.importPemPrivateKey(pki.privateKeyToPem(keypair.privateKey));
|
||||
state.acme=new Acme(state.accountKeyPair);
|
||||
makeKeyPair();
|
||||
}
|
||||
|
||||
function makeKeyPair() {
|
||||
var keypair;
|
||||
log('Generating cert keypair...');
|
||||
keypair=pki.rsa.generateKeyPair(2048);
|
||||
state.certPrivateKeyPEM=pki.privateKeyToPem(keypair.privateKey);
|
||||
state.certPrivateKey=cryptoUtil.importPemPrivateKey(state.certPrivateKeyPEM);
|
||||
register();
|
||||
}
|
||||
|
||||
function register() {
|
||||
post(options.newReg, {
|
||||
resource:'new-reg',
|
||||
contact:['mailto:'+options.email]
|
||||
}, getTerms);
|
||||
}
|
||||
|
||||
function getTerms(err, res) {
|
||||
var links;
|
||||
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
return handleErr(err, 'Registration request failed');
|
||||
}
|
||||
|
||||
links=parseLink(res.headers['link']);
|
||||
if (!links || !('next' in links)) {
|
||||
return handleErr(err, 'Server didn\'t provide information to proceed (1)');
|
||||
}
|
||||
|
||||
state.registrationURL=res.headers['location'];
|
||||
state.newAuthorizationURL=links['next'];
|
||||
state.termsRequired=('terms-of-service' in links);
|
||||
|
||||
if (state.termsRequired) {
|
||||
state.termsURL=links['terms-of-service'];
|
||||
log(state.termsURL);
|
||||
request.get(state.termsURL, getAgreement);
|
||||
} else {
|
||||
getChallenges();
|
||||
}
|
||||
}
|
||||
|
||||
function getAgreement(err, res, body) {
|
||||
if (err) {
|
||||
return handleErr(err, 'Couldn\'t get agreement');
|
||||
}
|
||||
log('The CA requires your agreement to terms:\n'+state.termsURL);
|
||||
sendAgreement();
|
||||
}
|
||||
|
||||
function sendAgreement() {
|
||||
if (state.termsRequired && !options.agreeTerms) {
|
||||
return handleErr(null, 'The CA requires your agreement to terms: '+state.termsURL);
|
||||
}
|
||||
|
||||
log('Posting agreement to: '+state.registrationURL);
|
||||
|
||||
post(state.registrationURL, {
|
||||
resource:'reg',
|
||||
agreement:state.termsURL
|
||||
}, function(err, res, body) {
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
return handleErr(err, 'Couldn\'t POST agreement back to server', body);
|
||||
} else {
|
||||
nextDomain();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function nextDomain() {
|
||||
if (state.domains.length > 0) {
|
||||
getChallenges(state.domains.shift());
|
||||
return;
|
||||
} else {
|
||||
getCertificate();
|
||||
}
|
||||
}
|
||||
|
||||
function getChallenges(domain) {
|
||||
state.domain=domain;
|
||||
|
||||
post(state.newAuthorizationURL, {
|
||||
resource:'new-authz',
|
||||
identifier:{
|
||||
type:'dns',
|
||||
value:state.domain,
|
||||
}
|
||||
}, getReadyToValidate);
|
||||
}
|
||||
|
||||
function getReadyToValidate(err, res, body) {
|
||||
var links, authz, httpChallenges, challenge, thumbprint, keyAuthorization, challengePath;
|
||||
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
return handleErr(err, 'Authorization request failed with code '+res.statusCode);
|
||||
}
|
||||
|
||||
links=parseLink(res.headers['link']);
|
||||
if (!links || !('next' in links)) {
|
||||
return handleErr(err, 'Server didn\'t provide information to proceed (2)');
|
||||
}
|
||||
|
||||
state.authorizationURL=res.headers['location'];
|
||||
state.newCertificateURL=links['next'];
|
||||
|
||||
authz=JSON.parse(body);
|
||||
|
||||
httpChallenges=authz.challenges.filter(function(x) {
|
||||
return x.type==='http-01';
|
||||
});
|
||||
if (httpChallenges.length===0) {
|
||||
return handleErr(null, 'Server didn\'t offer any challenge we can handle.');
|
||||
}
|
||||
challenge=httpChallenges[0];
|
||||
|
||||
thumbprint=cryptoUtil.thumbprint(state.accountKeyPair.publicKey);
|
||||
keyAuthorization=challenge.token+'.'+thumbprint;
|
||||
challengePath='.well-known/acme-challenge/'+challenge.token;
|
||||
state.responseURL=challenge['uri'];
|
||||
state.path=challengePath;
|
||||
|
||||
if (options.webroot) {
|
||||
try {
|
||||
mkdirp(path.dirname(options.webroot+'/'+challengePath));
|
||||
fs.writeFileSync(path.normalize(options.webroot+'/'+challengePath), keyAuthorization);
|
||||
challengeDone();
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not write challange file to disk');
|
||||
}
|
||||
} else if (typeof options.challenge==='function') {
|
||||
options.challenge(state.domain, '/'+challengePath, keyAuthorization, challengeDone);
|
||||
} else {
|
||||
return handleErr(null, 'No "challenge" function or "webroot" option given.');
|
||||
}
|
||||
|
||||
function challengeDone() {
|
||||
post(state.responseURL, {
|
||||
resource:'challenge',
|
||||
keyAuthorization:keyAuthorization
|
||||
}, function(err, res, body) {
|
||||
ensureValidation(err, res, body, function unlink() {
|
||||
if (options.webroot) {
|
||||
fs.unlinkSync(path.normalize(options.webroot+'/'+challengePath));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function ensureValidation(err, res, body, unlink) {
|
||||
var authz;
|
||||
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
unlink();
|
||||
return handleErr(err, 'Authorization status request failed ('+res.statusCode+')');
|
||||
}
|
||||
|
||||
authz=JSON.parse(body);
|
||||
|
||||
if (authz.status==='pending') {
|
||||
setTimeout(function() {
|
||||
request.get(state.authorizationURL, {}, function(err, res, body) {
|
||||
ensureValidation(err, res, body, unlink);
|
||||
});
|
||||
}, 1000);
|
||||
} else if (authz.status==='valid') {
|
||||
log('Validating domain ... done');
|
||||
state.validatedDomains.push(state.domain);
|
||||
state.validAuthorizationURLs.push(state.authorizationURL);
|
||||
unlink();
|
||||
nextDomain();
|
||||
} else if (authz.status==='invalid') {
|
||||
unlink();
|
||||
return handleErr(null, 'The CA was unable to validate the file you provisioned', body);
|
||||
} else {
|
||||
unlink();
|
||||
return handleErr(null, 'CA returned an authorization in an unexpected state', authz);
|
||||
}
|
||||
}
|
||||
|
||||
function getCertificate() {
|
||||
var csr=cryptoUtil.generateCSR(state.certPrivateKey, state.validatedDomains);
|
||||
log('Requesting certificate...');
|
||||
post(state.newCertificateURL, {
|
||||
resource:'new-cert',
|
||||
csr:csr,
|
||||
authorizations:state.validAuthorizationURLs
|
||||
}, downloadCertificate);
|
||||
}
|
||||
|
||||
function downloadCertificate(err, res, body) {
|
||||
var links, certURL;
|
||||
|
||||
if (err || Math.floor(res.statusCode/100)!==2) {
|
||||
log('Certificate request failed with error ', err);
|
||||
if (body) {
|
||||
log(body.toString());
|
||||
}
|
||||
return handleErr(err, 'Certificate request failed');
|
||||
}
|
||||
|
||||
links=parseLink(res.headers['link']);
|
||||
if (!links || !('up' in links)) {
|
||||
return handleErr(err, 'Failed to fetch CA certificate');
|
||||
}
|
||||
|
||||
log('Requesting certificate: done');
|
||||
|
||||
state.certificate=body;
|
||||
certURL=res.headers['location'];
|
||||
request.get({
|
||||
url:certURL,
|
||||
encoding:null
|
||||
}, function(err, res, body) {
|
||||
if (err) {
|
||||
return handleErr(err, 'Failed to fetch cert from '+certURL);
|
||||
}
|
||||
if (res.statusCode!==200) {
|
||||
return handleErr(err, 'Failed to fetch cert from '+certURL, res.body.toString());
|
||||
}
|
||||
if (body.toString()!==state.certificate.toString()) {
|
||||
handleErr(null, 'Cert at '+certURL+' did not match returned cert');
|
||||
} else {
|
||||
log('Successfully verified cert at '+certURL);
|
||||
log('Requesting CA certificate...');
|
||||
request.get({
|
||||
url:links['up'],
|
||||
encoding:null
|
||||
}, function(err, res, body) {
|
||||
if (err || res.statusCode!==200) {
|
||||
return handleErr(err, 'Failed to fetch CA certificate');
|
||||
}
|
||||
state.caCert=certBufferToPEM(body);
|
||||
log('Requesting CA certificate: done');
|
||||
done();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function done() {
|
||||
var cert, pfx;
|
||||
try {
|
||||
cert=certBufferToPEM(state.certificate);
|
||||
if (options.certFile) {
|
||||
fs.writeFileSync(options.certFile, cert);
|
||||
}
|
||||
if (options.keyFile) {
|
||||
fs.writeFileSync(options.keyFile, state.certPrivateKeyPEM);
|
||||
}
|
||||
if (options.caFile) {
|
||||
fs.writeFileSync(options.caFile, state.caCert);
|
||||
}
|
||||
if (options.pfxFile) {
|
||||
try {
|
||||
pfx=forge.pkcs12.toPkcs12Asn1(
|
||||
pki.privateKeyFromPem(state.certPrivateKeyPEM),
|
||||
[pki.certificateFromPem(cert), pki.certificateFromPem(state.caCert)],
|
||||
options.pfxPassword || '',
|
||||
options.aes ? {} : {algorithm:'3des'}
|
||||
);
|
||||
pfx=new Buffer(forge.asn1.toDer(pfx).toHex(), 'hex');
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not convert to PKCS#12');
|
||||
}
|
||||
fs.writeFileSync(options.pfxFile, pfx);
|
||||
}
|
||||
cb(null, cert, state.certPrivateKeyPEM, state.caCert);
|
||||
} catch(err) {
|
||||
handleErr(err, 'Could not write output files');
|
||||
}
|
||||
}
|
||||
|
||||
function post(url, body, cb) {
|
||||
return state.acme.post(url, body, cb);
|
||||
}
|
||||
|
||||
function handleErr(err, text, info) {
|
||||
log(text, err, info);
|
||||
cb(err || new Error(text));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function certBufferToPEM(cert) {
|
||||
cert=util.toStandardB64(cert.toString('base64'));
|
||||
cert=cert.match(/.{1,64}/g).join('\n');
|
||||
return '-----BEGIN CERTIFICATE-----\n'+cert+'\n-----END CERTIFICATE-----';
|
||||
}
|
||||
|
||||
function parseLink(link) {
|
||||
var links;
|
||||
try {
|
||||
links=link.split(',').map(function(link) {
|
||||
var parts, url, info;
|
||||
parts=link.trim().split(';');
|
||||
url=parts.shift().replace(/[<>]/g, '');
|
||||
info=parts.reduce(function(acc, p) {
|
||||
var m=p.trim().match(/(.+) *= *"(.+)"/);
|
||||
if (m) {
|
||||
acc[m[1]]=m[2];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
info['url']=url;
|
||||
return info;
|
||||
}).reduce(function(acc, link) {
|
||||
if ('rel' in link) {
|
||||
acc[link['rel']]=link['url'];
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
return links;
|
||||
} catch(err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
exports.getCert=getCert;
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
// Copyright 2014 ISRG. All rights reserved
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
var crypto = require("crypto");
|
||||
var forge = require("node-forge");
|
||||
var util = require("./acme-util.js");
|
||||
|
||||
var TOKEN_SIZE = 16;
|
||||
var NONCE_SIZE = 16;
|
||||
|
||||
function bytesToBuffer(bytes) {
|
||||
return new Buffer(forge.util.bytesToHex(bytes), "hex");
|
||||
}
|
||||
|
||||
function bufferToBytes(buf) {
|
||||
return forge.util.hexToBytes(buf.toString("hex"));
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
return util.b64enc(bytesToBuffer(bytes));
|
||||
}
|
||||
|
||||
function base64ToBytes(base64) {
|
||||
return bufferToBytes(util.b64dec(base64));
|
||||
}
|
||||
|
||||
function bnToBase64(bn) {
|
||||
var hex = bn.toString(16);
|
||||
if (hex.length % 2 == 1) { hex = "0" + hex; }
|
||||
return util.b64enc(new Buffer(hex, "hex"));
|
||||
}
|
||||
|
||||
function base64ToBn(base64) {
|
||||
return new forge.jsbn.BigInteger(util.b64dec(base64).toString("hex"), 16);
|
||||
}
|
||||
|
||||
function importPrivateKey(privateKey) {
|
||||
return forge.pki.rsa.setPrivateKey(
|
||||
base64ToBn(privateKey.n),
|
||||
base64ToBn(privateKey.e), base64ToBn(privateKey.d),
|
||||
base64ToBn(privateKey.p), base64ToBn(privateKey.q),
|
||||
base64ToBn(privateKey.dp),base64ToBn(privateKey.dq),
|
||||
base64ToBn(privateKey.qi));
|
||||
}
|
||||
|
||||
function importPublicKey(publicKey) {
|
||||
return forge.pki.rsa.setPublicKey(
|
||||
base64ToBn(publicKey.n),
|
||||
base64ToBn(publicKey.e));
|
||||
}
|
||||
|
||||
function exportPrivateKey(privateKey) {
|
||||
return {
|
||||
"kty": "RSA",
|
||||
"n": bnToBase64(privateKey.n),
|
||||
"e": bnToBase64(privateKey.e),
|
||||
"d": bnToBase64(privateKey.d),
|
||||
"p": bnToBase64(privateKey.p),
|
||||
"q": bnToBase64(privateKey.q),
|
||||
"dp": bnToBase64(privateKey.dP),
|
||||
"dq": bnToBase64(privateKey.dQ),
|
||||
"qi": bnToBase64(privateKey.qInv)
|
||||
};
|
||||
}
|
||||
|
||||
function exportPublicKey(publicKey) {
|
||||
return {
|
||||
"kty": "RSA",
|
||||
"n": bnToBase64(publicKey.n),
|
||||
"e": bnToBase64(publicKey.e)
|
||||
};
|
||||
}
|
||||
|
||||
// A note on formats:
|
||||
// * Keys are always represented as JWKs
|
||||
// * Signature objects are in ACME format
|
||||
// * Certs and CSRs are base64-encoded
|
||||
module.exports = {
|
||||
///// RANDOM STRINGS
|
||||
|
||||
randomString: function(nBytes) {
|
||||
return bytesToBase64(forge.random.getBytesSync(nBytes));
|
||||
},
|
||||
|
||||
randomSerialNumber: function() {
|
||||
return forge.util.bytesToHex(forge.random.getBytesSync(4));
|
||||
},
|
||||
|
||||
newToken: function() {
|
||||
return this.randomString(TOKEN_SIZE);
|
||||
},
|
||||
|
||||
///// SHA-256
|
||||
|
||||
sha256: function(buf) {
|
||||
return crypto.createHash('sha256').update(buf).digest('hex');
|
||||
},
|
||||
|
||||
///// KEY PAIR MANAGEMENT
|
||||
|
||||
generateKeyPair: function(bits) {
|
||||
var keyPair = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
|
||||
return {
|
||||
privateKey: exportPrivateKey(keyPair.privateKey),
|
||||
publicKey: exportPublicKey(keyPair.publicKey)
|
||||
};
|
||||
},
|
||||
|
||||
importPemPrivateKey: function(pem) {
|
||||
var key = forge.pki.privateKeyFromPem(pem);
|
||||
return {
|
||||
privateKey: exportPrivateKey(key),
|
||||
publicKey: exportPublicKey(key)
|
||||
};
|
||||
},
|
||||
|
||||
importPemCertificate: function(pem) {
|
||||
return forge.pki.certificateFromPem(pem);
|
||||
},
|
||||
|
||||
privateKeyToPem: function(privateKey) {
|
||||
var priv = importPrivateKey(privateKey);
|
||||
return forge.pki.privateKeyToPem(priv);
|
||||
},
|
||||
|
||||
certificateToPem: function(certificate) {
|
||||
var derCert = base64ToBytes(certificate);
|
||||
var cert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
||||
return forge.pki.certificateToPem(cert);
|
||||
},
|
||||
|
||||
certificateRequestToPem: function(csr) {
|
||||
var derReq = base64ToBytes(csr);
|
||||
var c = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derReq));
|
||||
return forge.pki.certificateRequestToPem(c);
|
||||
},
|
||||
|
||||
thumbprint: function(publicKey) {
|
||||
// Only handling RSA keys
|
||||
input = bytesToBuffer('{"e":"'+ publicKey.e + '","kty":"RSA","n":"'+ publicKey.n +'"}');
|
||||
return util.b64enc(crypto.createHash('sha256').update(input).digest());
|
||||
},
|
||||
|
||||
///// SIGNATURE GENERATION / VERIFICATION
|
||||
|
||||
generateSignature: function(keyPair, payload, nonce) {
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
|
||||
// Compute JWS signature
|
||||
var protectedHeader = "";
|
||||
if (nonce) {
|
||||
protectedHeader = JSON.stringify({nonce: nonce});
|
||||
}
|
||||
var protected64 = util.b64enc(new Buffer(protectedHeader));
|
||||
var payload64 = util.b64enc(payload);
|
||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
||||
var md = forge.md.sha256.create();
|
||||
md.update(signatureInput);
|
||||
var sig = privateKey.sign(md);
|
||||
|
||||
return {
|
||||
header: {
|
||||
alg: "RS256",
|
||||
jwk: keyPair.publicKey,
|
||||
},
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: util.b64enc(bytesToBuffer(sig)),
|
||||
}
|
||||
},
|
||||
|
||||
verifySignature: function(jws) {
|
||||
if (jws.protected) {
|
||||
if (!jws.header) {
|
||||
jws.header = {};
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(jws.protected);
|
||||
var protectedJSON = util.b64dec(jws.protected).toString();
|
||||
console.log(protectedJSON);
|
||||
var protectedObj = JSON.parse(protectedJSON);
|
||||
for (key in protectedObj) {
|
||||
jws.header[key] = protectedObj[key];
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error unmarshaling json: "+e)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes validSignature(sig)
|
||||
if (!jws.header.jwk || (jws.header.jwk.kty != "RSA")) {
|
||||
// Unsupported key type
|
||||
console.log("Unsupported key type");
|
||||
return false;
|
||||
} else if (!jws.header.alg || !jws.header.alg.match(/^RS/)) {
|
||||
// Unsupported algorithm
|
||||
console.log("Unsupported alg: "+jws.header.alg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute signature input
|
||||
var protected64 = (jws.protected)? jws.protected : "";
|
||||
var payload64 = (jws.payload)? jws.payload : "";
|
||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
||||
|
||||
// Compute message digest
|
||||
var md;
|
||||
switch (jws.header.alg) {
|
||||
case "RS1": md = forge.md.sha1.create(); break;
|
||||
case "RS256": md = forge.md.sha256.create(); break;
|
||||
case "RS384": md = forge.md.sha384.create(); break;
|
||||
case "RS512": md = forge.md.sha512.create(); break;
|
||||
default: return false; // Unsupported algorithm
|
||||
}
|
||||
md.update(signatureInput);
|
||||
|
||||
// Import the key and signature
|
||||
var publicKey = importPublicKey(jws.header.jwk);
|
||||
var sig = bufferToBytes(util.b64dec(jws.signature));
|
||||
|
||||
return publicKey.verify(md.digest().bytes(), sig);
|
||||
},
|
||||
|
||||
///// CSR GENERATION / VERIFICATION
|
||||
|
||||
generateCSR: function(keyPair, names) {
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
var publicKey = importPublicKey(keyPair.publicKey);
|
||||
|
||||
// Create and sign the CSR
|
||||
var csr = forge.pki.createCertificationRequest();
|
||||
csr.publicKey = publicKey;
|
||||
csr.setSubject([{ name: 'commonName', value: names[0] }]);
|
||||
|
||||
var sans = [];
|
||||
for (i in names) {
|
||||
sans.push({ type: 2, value: names[i] });
|
||||
}
|
||||
csr.setAttributes([{
|
||||
name: 'extensionRequest',
|
||||
extensions: [{name: 'subjectAltName', altNames: sans}]
|
||||
}]);
|
||||
|
||||
csr.sign(privateKey, forge.md.sha256.create());
|
||||
|
||||
// Convert CSR -> DER -> Base64
|
||||
var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr));
|
||||
return util.b64enc(bytesToBuffer(der));
|
||||
},
|
||||
|
||||
verifiedCommonName: function(csr_b64) {
|
||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
||||
|
||||
if (!csr.verify()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
||||
if (csr.subject.attributes[i].name == "commonName") {
|
||||
return csr.subject.attributes[i].value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
///// CERTIFICATE GENERATION
|
||||
|
||||
// 'ca' parameter includes information about the CA
|
||||
// {
|
||||
// distinguishedName: /* forge-formatted DN */
|
||||
// keyPair: {
|
||||
// publicKey: /* JWK */
|
||||
// privateKey: /* JWK */
|
||||
// }
|
||||
// }
|
||||
generateCertificate: function(ca, serialNumber, csr_b64) {
|
||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
||||
|
||||
// Extract the public key and common name
|
||||
var publicKey = csr.publicKey;
|
||||
var commonName = null;
|
||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
||||
if (csr.subject.attributes[i].name == "commonName") {
|
||||
commonName = csr.subject.attributes[i].value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!commonName) { return false; }
|
||||
|
||||
// Create the certificate
|
||||
var cert = forge.pki.createCertificate();
|
||||
cert.publicKey = publicKey;
|
||||
cert.serialNumber = serialNumber;
|
||||
|
||||
// 1-year validity
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
cert.setSubject([{ name: "commonName", value: commonName }]);
|
||||
cert.setIssuer(ca.distinguishedName);
|
||||
cert.setExtensions([
|
||||
{ name: "basicConstraints", cA: false },
|
||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
||||
{ name: "extKeyUsage", serverAuth: true },
|
||||
{ name: "subjectAltName", altNames: [{ type: 2, value: commonName }] }
|
||||
]);
|
||||
|
||||
// Import signing key and sign
|
||||
var privateKey = importPrivateKey(ca.keyPair.privateKey);
|
||||
cert.sign(privateKey);
|
||||
|
||||
// Return base64-encoded DER
|
||||
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
||||
return bytesToBuffer(der);
|
||||
},
|
||||
|
||||
generateDvsniCertificate: function(keyPair, nonceName, zName) {
|
||||
var cert = forge.pki.createCertificate();
|
||||
cert.publicKey = importPublicKey(keyPair.publicKey);
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
cert.setSubject([{ name: "commonName", value: nonceName }]);
|
||||
cert.setIssuer([{ name: "commonName", value: nonceName }]);
|
||||
cert.setExtensions([
|
||||
{ name: "basicConstraints", cA: false },
|
||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
||||
{ name: "extKeyUsage", serverAuth: true },
|
||||
{ name: "subjectAltName", altNames: [
|
||||
{ type: 2, value: nonceName },
|
||||
{ type: 2, value: zName }
|
||||
]}
|
||||
]);
|
||||
cert.sign(importPrivateKey(keyPair.privateKey));
|
||||
|
||||
// Return base64-encoded DER, as above
|
||||
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
||||
return util.b64enc(bytesToBuffer(der));
|
||||
},
|
||||
|
||||
///// TLS CONTEXT GENERATION
|
||||
|
||||
createContext: function(keyPair, cert) {
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
var derCert = bufferToBytes(util.b64dec(cert));
|
||||
var realCert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
||||
return crypto.createCredentials({
|
||||
key: forge.pki.privateKeyToPem(privateKey),
|
||||
cert: forge.pki.certificateToPem(realCert)
|
||||
}).context;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "letiny",
|
||||
"version": "0.0.3-beta",
|
||||
"description": "Tiny ACME client library and CLI",
|
||||
"author": "Anatol Sommer <anatol@anatol.at>",
|
||||
"license": "MPL",
|
||||
"keywords": [
|
||||
"tiny",
|
||||
"acme",
|
||||
"letsencrypt",
|
||||
"client",
|
||||
"cli",
|
||||
"pfx"
|
||||
],
|
||||
"bin": {
|
||||
"letiny": "./lib/cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"colors": "^1.1.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"node-forge": "^0.6.21",
|
||||
"request": "^2.55.0",
|
||||
"commander": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^2.3.3",
|
||||
"better-assert": "^1.0.2"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue