Compare commits
	
		
			10 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7cdb42db7a | |||
|  | 37a7fd55b7 | ||
|  | e96ab294b6 | ||
|  | 7bdf8804a3 | ||
|  | 718e081495 | ||
|  | 258ad19f77 | ||
|  | 03a4db1034 | ||
|  | b0eff828bb | ||
|  | 00157ab870 | ||
|  | a0bda23683 | 
							
								
								
									
										101
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								README.md
									
									
									
									
									
								
							| @ -1,8 +1,99 @@ | ||||
| # See [`greenlock-store-test`](https://git.rootprojects.org/root/greenlock-store-test.js) | ||||
| # le-store-SPEC | ||||
| 
 | ||||
| That's the test. | ||||
| The reference implementation, specification, template, and tests for creating an le-store- strategy. | ||||
| 
 | ||||
| ## Reference implementations | ||||
| The reference implementation is completely in-memory. | ||||
| 
 | ||||
| * [`greenlock-store-fs`](https://git.rootprojects.org/root/greenlock-store-fs.js) | ||||
| * [`greenlock-store-sequelize`](https://git.rootprojects.org/root/greenlock-store-sequelize.js) | ||||
| See [Help Wanted: Database Plugins (for saving certs)](https://github.com/Daplie/node-letsencrypt/issues/39) | ||||
| 
 | ||||
| How to create a custom strategy | ||||
| =============================== | ||||
| 
 | ||||
| READ THIS README: | ||||
| Believe it or not, most of your answers are either right here | ||||
| or in the comments in the sample code in `index.js`. | ||||
| 
 | ||||
| Now, let's say there's some new database AwesomeDB that | ||||
| we want to make a plugin for, here's how we'd start: | ||||
| 
 | ||||
| ```bash | ||||
| # First create you repo on github or wherever | ||||
| # Then clone it | ||||
| git clone git@github.com:AwesomeDB/le-store-awesome.git | ||||
| 
 | ||||
| pushd le-store-awesome | ||||
| 
 | ||||
| # IMPORTANT: we pull in the 'template' branch, which has the skeleton code | ||||
| git pull https://github.com/Daplie/le-store-SPEC.git template | ||||
| 
 | ||||
| git push | ||||
| ``` | ||||
| 
 | ||||
| Or, if you already have some code and just need to merge in the tests: | ||||
| 
 | ||||
| ```bash | ||||
| git pull https://github.com/Daplie/le-store-SPEC.git tests | ||||
| ``` | ||||
| 
 | ||||
| Next, Just run the tests | ||||
| 
 | ||||
| ``` | ||||
| node tests/basic.js | ||||
| ``` | ||||
| 
 | ||||
| Note: you should not modify the tests that come from the tests branch, | ||||
| but rather create separate files for your own tests. | ||||
| 
 | ||||
| API | ||||
| === | ||||
| 
 | ||||
| ``` | ||||
| * getOptions() | ||||
| * accounts. | ||||
|   * checkKeypair(opts, cb) | ||||
|   * setKeypair(opts, keypair, cb) | ||||
|   * check(opts, cb) | ||||
|   * set(opts, reg, cb) | ||||
| * certificates. | ||||
|   * checkKeypair(opts, cb) | ||||
|   * setKeypair(opts, keypair, cb) | ||||
|   * check(opts, cb) | ||||
|   * set(opts, certs, cb) | ||||
| ``` | ||||
| 
 | ||||
| Keypairs | ||||
| -------- | ||||
| 
 | ||||
| For convenience, the keypair object will always contain **both** PEM and JWK | ||||
| versions of the private and/or public keys when being passed to the `*Keypair` functions. | ||||
| 
 | ||||
| **set** | ||||
| 
 | ||||
| `setKeypair` will always be called with `email` and **all three** forms of the keypair: | ||||
| `privateKeyPem`, `publicKeyPem`, and `privateKeyJwk`. It's easy to generate `publicKeyJwk` | ||||
| from `privateKeyJwk` because it is just a copy of the public fields `e` and `n`. | ||||
| 
 | ||||
| ``` | ||||
| // keypair looks like this | ||||
| { privateKeyPem: '...' | ||||
| , publicKeyPem: '...' | ||||
| , privateKeyJwk: { ... } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| **check** | ||||
| 
 | ||||
| `checkKeypair` may be called with any of `email`, `accountId`, and `keypair` - which will | ||||
| contain only `publicKeyPem` and `publicKeyJwk`. | ||||
| 
 | ||||
| ``` | ||||
| // opts looks like this | ||||
| { | ||||
|   email: '...@...' | ||||
| , accountId: '...' | ||||
| , keypair: { | ||||
|     publicKeyPem: '...' | ||||
|   , publicKeyJwk: { ... } | ||||
|   } | ||||
| } | ||||
| ``` | ||||
|  | ||||
							
								
								
									
										332
									
								
								index.js
									
									
									
									
									
								
							
							
						
						
									
										332
									
								
								index.js
									
									
									
									
									
								
							| @ -4,342 +4,108 @@ module.exports.create = function (options) { | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   var crypto = require('crypto'); | ||||
|   var defaults = {}; | ||||
|   var memDb = { | ||||
|     accountKeypairs: {} | ||||
|   , certificateKeypairs: {} | ||||
|   , accountIndices: {} | ||||
|   , certIndices: {} | ||||
|   , certificates: {} | ||||
|   , accounts: {} | ||||
|   , accountCerts: {} | ||||
|   }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   var accounts = { | ||||
| 
 | ||||
|     // Accounts
 | ||||
|     setKeypair: function (opts, keypair, cb) { | ||||
|       // opts.email // non-optional
 | ||||
|       // opts.keypair // non-optional
 | ||||
|       // opts.email     // optional
 | ||||
|       // opts.accountId // optional - same as returned from acounts.set(opts, reg)
 | ||||
| 
 | ||||
|       if (!opts.email) { | ||||
|         cb(new Error("MUST use email when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (!keypair.privateKeyJwk) { | ||||
|         cb(new Error("MUST use privateKeyJwk when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!keypair.privateKeyPem) { | ||||
|         cb(new Error("MUST use privateKeyPem when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!keypair.publicKeyPem) { | ||||
|         cb(new Error("MUST use publicKeyPem when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var accountId = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex'); | ||||
| 
 | ||||
|       memDb.accountIndices[accountId] = accountId; | ||||
|       memDb.accountIndices[opts.email] = accountId; | ||||
|       memDb.accountKeypairs[accountId] = keypair; | ||||
|       /* | ||||
|       { | ||||
|         id: accountId | ||||
|         // TODO nix accountId
 | ||||
|       , accountId: accountId | ||||
|       , email: opts.email | ||||
|       , keypair: keypair | ||||
|       }; | ||||
|       */ | ||||
| 
 | ||||
|       cb(null, memDb.accountKeypairs[accountId]); | ||||
|       // SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
 | ||||
|       // keypair = { privateKeyPem: '...', privateKeyJwk: { ... } }
 | ||||
|       cb(null, keypair); | ||||
|     } | ||||
|     // Accounts
 | ||||
|   , checkKeypair: function (opts, cb) { | ||||
|       // opts.email // optional
 | ||||
|       // opts.accountId // optional
 | ||||
|       // opts.accountId // optional - same as returned from acounts.set(opts, reg)
 | ||||
| 
 | ||||
|       var keypair = opts.keypair || {}; | ||||
|       var index; | ||||
| 
 | ||||
|       if (keypair.publicKeyPem) { | ||||
|         index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex'); | ||||
|         index = memDb.accountIndices[index]; | ||||
|       } | ||||
|       else if (keypair.publicKeyJwk) { | ||||
|         // TODO RSA.exportPublicPem(keypair);
 | ||||
|         cb(new Error("id from publicKeyJwk not yet implemented")); | ||||
|         return; | ||||
|       } | ||||
|       else if (opts.email) { | ||||
|         index = memDb.accountIndices[opts.email]; | ||||
|       } | ||||
|       else { | ||||
|         cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       cb(null, memDb.accountKeypairs[index] || null); | ||||
|       // check db and return null or keypair object with one
 | ||||
|       // (or both) of privateKeyPem or privateKeyJwk
 | ||||
|       cb(null, { privateKeyPem: '...', privateKeyJwk: {} }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     // Accounts
 | ||||
|   , check: function (opts, cb) { | ||||
|       // opts.email       // optional
 | ||||
|       // opts.accountId   // optional - same as returned from acounts.set(opts, reg)
 | ||||
|       // opts.domains     // optional - same as set in certificates.set(opts, certs)
 | ||||
| 
 | ||||
|       // return account from db if it exists, otherwise null
 | ||||
|       cb(null, { id: '...', keypair: { privateKeyJwk: {} }/*, domains: []*/ }); | ||||
|     } | ||||
|     // Accounts
 | ||||
|   , set: function (opts, reg, cb) { | ||||
|       // opts.email
 | ||||
|       // reg.keypair
 | ||||
|       // reg.receipt // response from acme server
 | ||||
| 
 | ||||
|       var keypair = reg.keypair || opts.keypair || {}; | ||||
|       var accountId; | ||||
|       var index; | ||||
| 
 | ||||
|       if (keypair.publicKeyPem) { | ||||
|         index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex'); | ||||
|         index = memDb.accountIndices[index]; | ||||
|       } | ||||
|       else if (keypair.publicKeyJwk) { | ||||
|         // TODO RSA.exportPublicPem(keypair);
 | ||||
|         cb(new Error("id from publicKeyJwk not yet implemented")); | ||||
|         return; | ||||
|       } | ||||
|       else if (opts.email) { | ||||
|         index = memDb.accountIndices[opts.email]; | ||||
|       } | ||||
|       else { | ||||
|         cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk")); | ||||
|         return; | ||||
|       // You must implement a method to deterministically generate 'id'
 | ||||
|       // For example, you could do this:
 | ||||
|       // var id = crypto.createHash('sha256').update(reg.keypair.publicKeyPem).digest('hex');
 | ||||
|       cb(null, { id: '...', email: opts.email, keypair: reg.keypair, receipt: reg.receipt }); | ||||
|     } | ||||
| 
 | ||||
|       accountId = memDb.accountIndices[index]; | ||||
|       if (!accountId) { | ||||
|         cb(new Error("keypair was not previously set with email and keypair.publicKeyPem")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       memDb.accounts[accountId] = { | ||||
|         id: accountId | ||||
|         // TODO nix accountId
 | ||||
|       , accountId: accountId | ||||
|       , email: opts.email | ||||
|       , keypair: keypair | ||||
|       , agreeTos: opts.agreeTos || reg.agreeTos | ||||
|       //, receipt: reg.receipt || opts.receipt
 | ||||
|       }; | ||||
|       Object.keys(reg).forEach(function (key) { | ||||
|         memDb.accounts[accountId][key] = reg[key]; | ||||
|       }); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|       cb(null, memDb.accounts[accountId]); | ||||
|     } | ||||
|     // Accounts
 | ||||
|   , check: function (opts, cb) { | ||||
|       // opts.email // optional
 | ||||
|       // opts.accountId // optional
 | ||||
|       // opts.domains // optional
 | ||||
| 
 | ||||
|       var keypair = opts.keypair || {}; | ||||
|       var index; | ||||
|       var accountId; | ||||
|       var account; | ||||
| 
 | ||||
|       if (opts.accountId) { | ||||
|         index = memDb.accountIndices[opts.accountId]; | ||||
|       } | ||||
|       else if (keypair.publicKeyPem) { | ||||
|         index = crypto.createHash('sha256').update(keypair.publicKeyPem).digest('hex'); | ||||
|         index = memDb.accountIndices[index]; | ||||
|       } | ||||
|       else if (keypair.publicKeyJwk) { | ||||
|         // TODO RSA.exportPublicPem(keypair);
 | ||||
|         cb(new Error("id from publicKeyJwk not yet implemented")); | ||||
|         return; | ||||
|       } | ||||
|       else if (opts.email) { | ||||
|         index = memDb.accountIndices[opts.email]; | ||||
|       } | ||||
|       else if (opts.domains && opts.domains[0]) { | ||||
|         index = memDb.accountIndices[opts.domains[0]]; | ||||
|       } | ||||
|       else { | ||||
|         console.error(opts); | ||||
|         cb(new Error("MUST supply email or keypair.publicKeyPem or keypair.publicKeyJwk")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       accountId = memDb.accountIndices[index]; | ||||
|       if (!accountId) { | ||||
|         cb(null, null); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       account = JSON.parse(JSON.stringify(memDb.accounts[accountId] || null)); | ||||
|       account.keypair = memDb.accountKeypairs[accountId] || null; | ||||
| 
 | ||||
|       cb(null, account); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|   var certificates = { | ||||
| 
 | ||||
|     // Certificates
 | ||||
|     setKeypair: function (opts, keypair, cb) { | ||||
|       // opts.domains
 | ||||
| 
 | ||||
|       if (!opts.domains || !opts.domains.length) { | ||||
|         cb(new Error("MUST use domains when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!opts.email) { | ||||
|         cb(new Error("MUST use email when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!opts.accountId) { | ||||
|         cb(new Error("MUST use accountId when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       // opts.domains - this is an array, but you nly need the first (or any) of them
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|       if (!keypair.privateKeyJwk) { | ||||
|         cb(new Error("MUST use privateKeyJwk when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!keypair.privateKeyPem) { | ||||
|         cb(new Error("MUST use privateKeyPem when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       if (!keypair.publicKeyPem) { | ||||
|         cb(new Error("MUST use publicKeyPem when setting Keypair")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|       var subject = opts.domains[0]; | ||||
| 
 | ||||
|       opts.domains.forEach(function (domain) { | ||||
|         memDb.certIndices[domain] = subject; | ||||
|       }); | ||||
| 
 | ||||
|       memDb.certKeypairs[subject] = keypair; | ||||
|       /* | ||||
|       { | ||||
|         subject: subject | ||||
|       , keypair: keypair | ||||
|       }; | ||||
|       */ | ||||
| 
 | ||||
|       cb(null, memDb.certKeypairs[subject]); | ||||
|       // SAVE to db (as PEM and/or JWK) and index each domain in domains to this keypair
 | ||||
|       cb(null, keypair); | ||||
|     } | ||||
|     // Certificates
 | ||||
|   , checkKeypair: function (opts, cb) { | ||||
|       // opts.domains
 | ||||
|       if (!opts.domains || !opts.domains.length) { | ||||
|         cb(new Error("MUST use domains when checking Keypair")); | ||||
|         return; | ||||
|       } | ||||
|       // opts.domains - this is an array, but you only need the first (or any) of them
 | ||||
| 
 | ||||
|       var domain = opts.domains[0]; | ||||
|       var subject = memDb.certIndices[domain]; | ||||
| 
 | ||||
|       cb(null, memDb.certKeypairs[subject]); | ||||
|       // check db and return null or keypair object with one of privateKeyPem or privateKeyJwk
 | ||||
|       cb(null, { privateKeyPem: '...', privateKeyJwk: {} }); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     // Certificates
 | ||||
|   , set: function (opts, cb) { | ||||
|       // opts.domains
 | ||||
|       // opts.email // optional
 | ||||
|       // opts.accountId // optional
 | ||||
| 
 | ||||
|       // opts.certs.privkey
 | ||||
|       // opts.certs.cert
 | ||||
|       // opts.certs.chain
 | ||||
| 
 | ||||
|       var index; | ||||
|       var accountId; | ||||
|       var account; | ||||
|       var certs = opts.certs; | ||||
|       var subject = certs.subject || opts.domains[0]; | ||||
|       var altnames = certs.altnames || opts.domains; | ||||
|       var accountCerts; | ||||
| 
 | ||||
|       if (opts.accountId) { | ||||
|         index = opts.accountId; | ||||
|       } | ||||
|       else if (opts.email) { | ||||
|         index = opts.email; | ||||
|       } | ||||
|       else { | ||||
|         cb(new Error("MUST supply email or accountId")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       accountId = memDb.accountIndices[index]; | ||||
|       account = memDb.accounts[accountId]; | ||||
| 
 | ||||
|       if (!account) { | ||||
|         cb(new Error("account must exist")); | ||||
|       } | ||||
| 
 | ||||
|       accountId = memDb.accountIndices[index]; | ||||
|       if (!accountId) { | ||||
|         cb(new Error("keypair was not previously set with email and keypair.publicKeyPem")); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       memDb.certIndices[subject] = subject; | ||||
|       altnames.forEach(function (altname) { | ||||
|         memDb.certIndices[altname] = subject; | ||||
|       }); | ||||
| 
 | ||||
|       accountCerts = memDb.accountCerts[accountId] || {}; | ||||
|       accountCerts[subject] = subject; | ||||
|       memDb.accountCerts[accountId] = accountCerts; | ||||
| 
 | ||||
|       memDb.certificates[subject] = certs; | ||||
| 
 | ||||
|       // SAVE to the database, index the email address, the accountId, and alias the domains
 | ||||
|       cb(null, certs); | ||||
|     } | ||||
|     // Certificates
 | ||||
|   , check: function (opts, cb) { | ||||
|       // You will be provided one of these (which should be tried in this order)
 | ||||
|       // opts.domains
 | ||||
|       // opts.email // optional
 | ||||
|       // opts.accountId // optional
 | ||||
|       var subject; | ||||
|       var subjects; | ||||
|       var accountId; | ||||
| 
 | ||||
|       if (opts.domains) { | ||||
|         subject = memDb.certIndices[opts.domains[0]]; | ||||
|         cb(null, memDb.certificates[subject]); | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       if (opts.accountId) { | ||||
|         accountId = memDb.accountIndices[opts.accountId]; | ||||
|       } | ||||
|       else if (opts.email) { | ||||
|         accountId = memDb.accountIndices[opts.email]; | ||||
|       // return certificate PEMs from db if they exist, otherwise null
 | ||||
|       // optionally include expiresAt and issuedAt, if they are known exactly
 | ||||
|       // (otherwise they will be read from the cert itself later)
 | ||||
|       cb(null, { privkey: 'PEM', cert: 'PEM', chain: 'PEM', domains: [], accountId: '...' }); | ||||
|     } | ||||
|     // Certificates
 | ||||
|   , set: function (opts, cb) { | ||||
|       // opts.domains   // each of these must be indexed
 | ||||
|       // opts.email     // optional, should be indexed
 | ||||
|       // opts.accountId // optional - same as set by you in accounts.set(opts, keypair) above
 | ||||
| 
 | ||||
|       subjects = memDb.accountCerts[accountId] || []; | ||||
|       cb(null, subjects.map(function (subject) { | ||||
|         subject = memDb.certIndices[subject]; | ||||
|         return memDb.certificates[subject] || null ; | ||||
|       })); | ||||
|       // opts.certs.privkey
 | ||||
|       // opts.certs.cert
 | ||||
|       // opts.certs.chain
 | ||||
| 
 | ||||
| 
 | ||||
|       // SAVE to the database, index the email address, the accountId, and alias the domains
 | ||||
|       cb(null,  { privkey: 'PEM', cert: 'PEM', chain: 'PEM' }); | ||||
|     } | ||||
| 
 | ||||
|   }; | ||||
| @ -348,12 +114,6 @@ module.exports.create = function (options) { | ||||
| 
 | ||||
|   return { | ||||
|     getOptions: function () { | ||||
|       Object.keys(defaults).forEach(function (key) { | ||||
|         if ('undefined' === typeof options[key]) { | ||||
|           options[key] = defaults[key]; | ||||
|         } | ||||
|       }); | ||||
| 
 | ||||
|       // merge options with default settings and then return them
 | ||||
|       return options; | ||||
|     } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user