refactor and docs
This commit is contained in:
		
							parent
							
								
									b6dc41c704
								
							
						
					
					
						commit
						e651755417
					
				
							
								
								
									
										129
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								README.md
									
									
									
									
									
								
							| @ -1,94 +1,57 @@ | ||||
| # letiny | ||||
| Tiny acme client library and CLI to obtain ssl certificates (without using external commands like openssl). | ||||
| # letiny-core | ||||
| 
 | ||||
| A framework for building letsencrypt clients, forked from `letiny`. | ||||
| 
 | ||||
|   * browser | ||||
|   * node with `forge` (works on windows) | ||||
|   * node with `ursa` (works fast) | ||||
|   * any javascript implementation | ||||
| 
 | ||||
| ## Usage: | ||||
| `npm install letiny` | ||||
| 
 | ||||
| ```bash | ||||
| npm install --save letiny-core | ||||
| ``` | ||||
| 
 | ||||
| ### 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); | ||||
| ```javascript | ||||
| 'use strict'; | ||||
| 
 | ||||
| var leCore = require('leCore'); | ||||
| 
 | ||||
| leCore. | ||||
| ``` | ||||
| 
 | ||||
| ## API | ||||
| 
 | ||||
| ``` | ||||
| LeCore.registerNewAccount(); | ||||
| 
 | ||||
| LeCore.getCertificate(); | ||||
| 
 | ||||
| LeCore.Acme                     // Signs requests with JWK | ||||
|   acme = new Acme(lePrivateKey) // privateKey format is abstract | ||||
|   acme.post(url, body, cb)      // POST with signature | ||||
|   acme.parseLinks(link)         // (internal) parses 'link' header | ||||
|   acme.getNonce(url, cb)        // (internal) HEAD request to get 'replay-nonce' strings | ||||
| 
 | ||||
| LeCore.leCrypto | ||||
|   generateSignature(lePrivateKey, nodeBufferBody, nonceString) | ||||
| ``` | ||||
| 
 | ||||
| For testing and development, you can also inject the dependencies you want to use: | ||||
| 
 | ||||
| ```javascript | ||||
| leCore.create({ | ||||
|   request: require('request') | ||||
| , leCrypto: rquire('./lib/letsencrypt-forge') | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| All of the code is available under the MPL-2.0. | ||||
| 
 | ||||
| Some of the files are original work not modified from `letiny` | ||||
| and are made available under MIT as well (check file headers). | ||||
|  | ||||
| @ -4,129 +4,131 @@ | ||||
|  * Some code used from https://github.com/letsencrypt/boulder/tree/master/test/js
 | ||||
|  * MPL 2.0 | ||||
| */ | ||||
| 
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var NOOP=function () {}; | ||||
| var log=NOOP; | ||||
| var request=require('request'); | ||||
| var cryptoUtil=require('./crypto-util'); | ||||
| module.exports.create = function (deps) { | ||||
| 
 | ||||
| function Acme(privateKey) { | ||||
|   this.privateKey=privateKey; | ||||
|   this.nonces=[]; | ||||
| } | ||||
|   var NOOP=function () {}; | ||||
|   var log=NOOP; | ||||
|   var request=require('request'); | ||||
|   var generateSignature=deps.leCrypto.generateSignature; | ||||
| 
 | ||||
| Acme.prototype.getNonce=function(url, cb) { | ||||
|   var self=this; | ||||
|   function Acme(privateKey) { | ||||
|     this.privateKey=privateKey; | ||||
|     this.nonces=[]; | ||||
|   } | ||||
| 
 | ||||
|   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; | ||||
|     } | ||||
|   Acme.prototype.getNonce=function(url, cb) { | ||||
|     var self=this; | ||||
| 
 | ||||
|     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) { | ||||
|     request.head({ | ||||
|       url:url, | ||||
|     }, function(err, res/*, body*/) { | ||||
|       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.stack); | ||||
|       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 (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; | ||||
|     } | ||||
| 
 | ||||
|     if ('replay-nonce' in res.headers) { | ||||
|       log('Storing nonce: '+res.headers['replay-nonce']); | ||||
|       self.nonces.push(res.headers['replay-nonce']); | ||||
|     } | ||||
|     log('Using nonce: '+this.nonces[0]); | ||||
|     payload=JSON.stringify(body, null, 2); | ||||
|     jws=generateSignature( | ||||
|       this.privateKey, new Buffer(payload), this.nonces.shift() | ||||
|     ); | ||||
|     signed=JSON.stringify(jws, null, 2); | ||||
| 
 | ||||
|     cb(err, res, body); | ||||
|   }); | ||||
| }; | ||||
|     log('Posting to '+url); | ||||
|     log(signed.green); | ||||
|     log('Payload:'+payload.blue); | ||||
| 
 | ||||
| Acme.parseLink = 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 request.post({ | ||||
|       url:url, | ||||
|       body:signed, | ||||
|       encoding:null | ||||
|     }, function(err, res, body) { | ||||
|       var parsed; | ||||
| 
 | ||||
|       if (err) { | ||||
|         console.error(err.stack); | ||||
|         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); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   Acme.parseLink = 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; | ||||
|       }, {}); | ||||
|       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; | ||||
|   } | ||||
| }; | ||||
|       return links; | ||||
|     } catch(err) { | ||||
|       return null; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| module.exports = Acme; | ||||
|   return Acme; | ||||
| }; | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| // 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/.
 | ||||
| 'use strict'; | ||||
| 
 | ||||
| var crypto = require("crypto"); | ||||
| var forge = require("node-forge"); | ||||
							
								
								
									
										8
									
								
								lib/letsencrypt-node-crypto.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								lib/letsencrypt-node-crypto.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| /*! | ||||
|  * letsencrypt-core | ||||
|  * Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
 | ||||
|  * Apache-2.0 OR MIT (and hence also MPL 2.0) | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| module.exports = {}; | ||||
							
								
								
									
										85
									
								
								lib/letsencrypt-ursa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								lib/letsencrypt-ursa.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,85 @@ | ||||
| /*! | ||||
|  * letsencrypt-core | ||||
|  * Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
 | ||||
|  * Apache-2.0 OR MIT (and hence also MPL 2.0) | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var crypto = require('crypto'); | ||||
| var ursa = require('ursa'); | ||||
| var forge = require('node-forge'); | ||||
| 
 | ||||
| function binstr2b64(binstr) { | ||||
|   return new Buffer(binstr, 'binary').toString('base64'); | ||||
| } | ||||
| 
 | ||||
| function toAcmePrivateKey(privkeyPem) { | ||||
|   var forgePrivkey = forge.pki.privateKeyFromPem(privkeyPem); | ||||
| 
 | ||||
|   return { | ||||
|     kty: "RSA" | ||||
|   , n: binstr2b64(forgePrivkey.n) | ||||
|   , e: binstr2b64(forgePrivkey.e) | ||||
|   , d: binstr2b64(forgePrivkey.d) | ||||
|   , p: binstr2b64(forgePrivkey.p) | ||||
|   , q: binstr2b64(forgePrivkey.q) | ||||
|   , dp: binstr2b64(forgePrivkey.dP) | ||||
|   , dq: binstr2b64(forgePrivkey.dQ) | ||||
|   , qi: binstr2b64(forgePrivkey.qInv) | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function generateRsaKeypair(bitlen, exp, cb) { | ||||
|   var keypair = ursa.generatePrivateKey(bitlen /*|| 2048*/, exp /*65537*/); | ||||
|   var pems = { | ||||
|     publicKeyPem: keypair.toPublicPem().toString('ascii')   // ascii PEM: ----BEGIN...
 | ||||
|   , privateKeyPem: keypair.toPrivatePem().toString('ascii') // ascii PEM: ----BEGIN...
 | ||||
|   }; | ||||
| 
 | ||||
|   // I would have chosen sha1 or sha2... but whatever
 | ||||
|   pems.publicKeyMd5 = crypto.createHash('md5').update(pems.publicKeyPem).digest('hex'); | ||||
|   // json { n: ..., e: ..., iq: ..., etc }
 | ||||
|   pems.privateKeyJwk = toAcmePrivateKey(pems.privateKeyPem); | ||||
|   pems.privateKeyJson = pems.privateKeyJwk; | ||||
| 
 | ||||
|   // TODO thumbprint
 | ||||
| 
 | ||||
|   cb(null, pems); | ||||
| } | ||||
| 
 | ||||
| function parseAccountPrivateKey(pkj, cb) { | ||||
|   Object.keys(pkj).forEach(function (key) { | ||||
|     pkj[key] = new Buffer(pkj[key], 'base64'); | ||||
|   }); | ||||
| 
 | ||||
|   var priv; | ||||
| 
 | ||||
|   try { | ||||
|     priv = ursa.createPrivateKeyFromComponents( | ||||
|       pkj.n // modulus
 | ||||
|     , pkj.e // exponent
 | ||||
|     , pkj.p | ||||
|     , pkj.q | ||||
|     , pkj.dp | ||||
|     , pkj.dq | ||||
|     , pkj.qi | ||||
|     , pkj.d | ||||
|     ); | ||||
|   } catch(e) { | ||||
|     cb(e); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   cb(null, { | ||||
|     privateKeyPem: priv.toPrivatePem.toString('ascii') | ||||
|   , publicKeyPem: priv.toPublicPem.toString('ascii') | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| module.exports.generateRsaKeypair = generateRsaKeypair; | ||||
| module.exports.privateJwkToPems = parseAccountPrivateKey; | ||||
| module.exports.privatePemToJwk = toAcmePrivateKey; | ||||
| 
 | ||||
| // TODO deprecate
 | ||||
| module.exports.toAcmePrivateKey = toAcmePrivateKey; | ||||
| module.exports.parseAccountPrivateKey = parseAccountPrivateKey; | ||||
							
								
								
									
										38
									
								
								lib/node.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/node.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | ||||
| /*! | ||||
|  * letsencrypt-core | ||||
|  * Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
 | ||||
|  * Apache-2.0 OR MIT (and hence also MPL 2.0) | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| var request = require('request'); | ||||
| var leCrypto = require('./letsencrypt-node-crypto'); | ||||
| var leForge = require('./letsencrypt-forge'); | ||||
| var leUrsa; | ||||
| 
 | ||||
| try { | ||||
|   leUrsa = require('./letsencrypt-ursa'); | ||||
| } catch(e) { | ||||
|   leUrsa = {}; | ||||
|   // things will run a little slower on keygen, but it'll work on windows
 | ||||
|   // (but don't try this on raspberry pi - 20+ MINUTES key generation)
 | ||||
| } | ||||
| 
 | ||||
| // order of crypto precdence is
 | ||||
| // * native
 | ||||
| // * ursa
 | ||||
| // * forge (fallback)
 | ||||
| Object.keys(leUrsa).forEach(function (key) { | ||||
|   if (!leCrypto[key]) { | ||||
|     leCrypto[key] = leUrsa[key]; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| Object.keys(leForge).forEach(function (key) { | ||||
|   if (!leCrypto[key]) { | ||||
|     leCrypto[key] = leForge[key]; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| module.exports.request = request; | ||||
| module.exports.leCrypto = leCrypto; | ||||
							
								
								
									
										20
									
								
								node.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								node.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| /*! | ||||
|  * letsencrypt-core | ||||
|  * Copyright(c) 2015 AJ ONeal <aj@daplie.com> https://daplie.com
 | ||||
|  * Apache-2.0 OR MIT (and hence also MPL 2.0) | ||||
| */ | ||||
| 'use strict'; | ||||
| 
 | ||||
| function create(deps) { | ||||
|   var LeCore = {}; | ||||
| 
 | ||||
|   LeCore.leCrypto = deps.leCrypto; | ||||
|   LeCore.Acme = require('./lib/acme-client').create(deps); | ||||
|   LeCore.registerNewAccount = require('./lib/register-new-account').create(deps); | ||||
|   LeCore.getCertificate = require('./lib/get-certificate').create(deps); | ||||
| 
 | ||||
|   return LeCore; | ||||
| } | ||||
| 
 | ||||
| module.exports = create(require('./lib/node')); | ||||
| module.exports.create = create; | ||||
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							| @ -1,30 +1,30 @@ | ||||
| { | ||||
|   "name": "letiny", | ||||
|   "version": "0.0.5-beta", | ||||
|   "name": "letiny-core", | ||||
|   "version": "1.0.0", | ||||
|   "description": "Tiny ACME client library and CLI", | ||||
|   "author": "Anatol Sommer <anatol@anatol.at>", | ||||
|   "authors": [ | ||||
|     "Anatol Sommer <anatol@anatol.at>", | ||||
|     "AJ ONeal <aj@daplie.com>" | ||||
|   ], | ||||
|   "license": "MPL-2.0", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/anatolsommer/letiny.git" | ||||
|     "url": "https://github.com/Daplie/letiny-core.git" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "tiny", | ||||
|     "acme", | ||||
|     "letsencrypt", | ||||
|     "client", | ||||
|     "cli", | ||||
|     "pem", | ||||
|     "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" | ||||
|     "node-forge": "^0.6.38", | ||||
|     "request": "^2.55.0" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|     "ursa": "^0.9.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "mocha": "^2.3.3", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user