246 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
		
		
			
		
	
	
			246 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
|  | # le-store-memory
 | ||
|  | 
 | ||
|  | An in-memory reference implementation of a Certificate and Keypair storage strategy for Greenlock v2.7+ (and v3) | ||
|  | 
 | ||
|  | # Usage
 | ||
|  | 
 | ||
|  | ```js | ||
|  | var greenlock = require('greenlock'); | ||
|  | 
 | ||
|  | // This in-memory plugin has only one option: 'cache'. | ||
|  | // We could have It's used so that we can peek and poke at the store. | ||
|  | var cache = {}; | ||
|  | var gl = greenlock.create({ | ||
|  |   store: require('le-store-memory').create({ cache: cache }) | ||
|  | , approveDomains: approveDomains | ||
|  |   ... | ||
|  | }); | ||
|  | ``` | ||
|  | 
 | ||
|  | # How to build your own module:
 | ||
|  | 
 | ||
|  | **TL;DR**: Just take a look the code here, and don't over think it. | ||
|  | 
 | ||
|  | Also, you have the flexibility to get really fancy. _Don't!_ | ||
|  | You probably don't need to (unless you already know that you do). | ||
|  | 
 | ||
|  | In most cases you're just implementing dumb storage. | ||
|  | If all you do is `JSON.stringify()` on `set` (save) and `JSON.parse()` after `check` (get) | ||
|  | and just treat it as a blob with an ID, you'll do just fine. You can always optimize later. | ||
|  | 
 | ||
|  | **Promises** vs **Thunks** ("node callbacks") vs **Synchronous** returns: | ||
|  | You can use whatever style you like best. Everything is promisified under the hood. | ||
|  | 
 | ||
|  | Whenever you have neither a result, nor an error, you must **always return null** (instead of 'undefined'). | ||
|  | 
 | ||
|  | ### storage strategy vs approveDomains()
 | ||
|  | 
 | ||
|  | The _most_ important thing to keep in mind: `approveDomains()` is where all of the implementation-specific logic goes. | ||
|  | 
 | ||
|  | If you're writing a storage strategy (presumably why you're here), it's because you have logic in `approveDomains()` | ||
|  | that isn't supported by existing strategies. That makes it tempting to start thinking about things backwards, letting | ||
|  | your implementation-specific logic creep into your storage strategy. DON'T DO IT. | ||
|  | 
 | ||
|  | Keep in mind that, ultimately, **it takes human decision** / interaction / configuration to add, remove, or | ||
|  | **modify the collection of domains** that are allowed, and how many / which domains are listed on each certificate - | ||
|  | all of which is a _completely_ separate process that lives outside of Greenlock (i.e uploading a site to a new folder). | ||
|  | 
 | ||
|  | The coupling between the method chosen for storage and the method chosen for approval is inherint, but keep it loose. | ||
|  | 
 | ||
|  | Lastly, it would be appropriate to include an example `approveDomains()` with your implementation for reference. | ||
|  | 
 | ||
|  | ### 0. approveDomains() is the kick off
 | ||
|  | 
 | ||
|  | `approveDomains()` is called only when there is no certificate for a given domain in Greenlock's internal cache | ||
|  | and when that certificate is "renewable" (typically 15 days before expiration, which is configurable). | ||
|  | 
 | ||
|  | The user (perhaps you) will have checked in their database (or config file or file system) and retrieved relevant | ||
|  | details (email associated with the domain, related domains that belong as altnames on the certificate, etc). | ||
|  | 
 | ||
|  | Those options will be available to _all_ storage and challenge strategies. In fact, they can even change which | ||
|  | strategy is used (i.e. some users using a Digital Ocean strategy for DNS challenges, others using Route53). | ||
|  | 
 | ||
|  | ```js | ||
|  | function approveDomains(opts) { | ||
|  |   var info = userDb.getInfo(opts.domain); | ||
|  |   if (!info) { throw new Error("ignoring junk request, bad domain"); } | ||
|  | 
 | ||
|  |   opts.email = info.certificateOwner; | ||
|  |   opts.subject = info.certificateSubject | ||
|  |   opts.domains = info.certificateAltnames; | ||
|  | 
 | ||
|  |   return opts; // or Promise.resolve(opts); | ||
|  | } | ||
|  | ``` | ||
|  | 
 | ||
|  | ### 1. Implement `accounts.setKeypair`
 | ||
|  | 
 | ||
|  | First, you should implement `accounts.setKeypair()`. Just treat it like dumb storage. | ||
|  | 
 | ||
|  | This only gets called after a new account has already been created successfully. | ||
|  | That will only happen when a completely new certificate is going to be issued (not renewal), | ||
|  | and there's no user account already associate with that set of domains. | ||
|  | 
 | ||
|  | ```js | ||
|  | store.accounts.setKeypair = function (opts) { | ||
|  |   console.log('accounts.setKeypair:', opts); | ||
|  | 
 | ||
|  |   var id = opts.account.id || opts.email || 'default'; | ||
|  |   var keypair = opts.keypair; | ||
|  | 
 | ||
|  |   cache.accountKeypairs[id] = JSON.stringify({ | ||
|  |     privateKeyPem: keypair.privateKeyPem | ||
|  |   , privateKeyJwk: keypair.privateKeyJwk | ||
|  |   }); | ||
|  | 
 | ||
|  |   return null; // or Promise.resolve(null); | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | ### 2. Implement `accounts.checkKeypair`
 | ||
|  | 
 | ||
|  | Whatever you did above, you just do the opposite instead. Tada! | ||
|  | 
 | ||
|  | ```js | ||
|  | store.accounts.checkKeypair = function (opts) { | ||
|  |   console.log('accounts.checkKeypair:', opts); | ||
|  | 
 | ||
|  |   var id = opts.account.id || opts.email || 'default'; | ||
|  |   var keyblob = cache.accountKeypairs[id]; | ||
|  | 
 | ||
|  |   if (!keyblob) { return null; } | ||
|  | 
 | ||
|  |   return JSON.parse(keyblob); | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | ### 3. (and 4.) Optionally save ACME account metadata
 | ||
|  | 
 | ||
|  | You should probably skip this and not worry about it. | ||
|  | 
 | ||
|  | However, if you have a special need for it, or if you want to shave off an ACME API call, | ||
|  | you can save the account `kid` (a misnomer intended to mean "key id", but actually refers | ||
|  | to an arbitrary ACME URL, used to identify the account). | ||
|  | 
 | ||
|  | ```js | ||
|  | store.accounts.set = function (opts) { | ||
|  |   console.log('accounts.set:', opts); | ||
|  |   return null; | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | ```js | ||
|  | store.accounts.check = function (opts) { | ||
|  |   var id = opts.account.id || opts.email || 'default'; | ||
|  |   console.log('accounts.check:', opts); | ||
|  |   return null; | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | If you don't implement these the account key will be used to "recover" the `kid` as necessary. | ||
|  | You don't have to worry though, it doesn't create a duplicate accounts or have any other negative | ||
|  | side affects other than an additional API call as needed. | ||
|  | 
 | ||
|  | ### 5. Implement a method to save certificate keypairs
 | ||
|  | 
 | ||
|  | Each certificate is supposed to have a unique keypair, which **must not** be the same as the account keypair. | ||
|  | 
 | ||
|  | Again, just treat it like a blob in dumb storage and you'll be fine. | ||
|  | 
 | ||
|  | This is the same as `accounts.setKeypair()`, but using a different idea. | ||
|  | You could even use the same data store in most cases because the IDs aren't likely to clash. | ||
|  | 
 | ||
|  | ```js | ||
|  | store.certificates.setKeypair = function (opts) { | ||
|  |   console.log('certificates.setKeypair:', opts); | ||
|  | 
 | ||
|  |   var id = opts.certificate.kid || opts.certificate.id || opts.subject; | ||
|  |   var keypair = opts.keypair; | ||
|  | 
 | ||
|  |   cache.certificateKeypairs[id] = JSON.stringify({ | ||
|  |     privateKeyPem: keypair.privateKeyPem | ||
|  |   , privateKeyJwk: keypair.privateKeyJwk | ||
|  |   }); | ||
|  | 
 | ||
|  |   return null; | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | ### 6. Implement a method to get certificate keypairs
 | ||
|  | 
 | ||
|  | You know the drill. Same as `accounts.checkKeypair()`, but a different ID. | ||
|  | 
 | ||
|  | This isn't called until after the certificate retrieval is successful. | ||
|  | 
 | ||
|  | Note: Every account **must have** a unique account key and account keys are | ||
|  | **not allowed** to be used as certificate keys. However, you could use the | ||
|  | same certificate key for all domains on a device (i.e. a server) or an account. | ||
|  | 
 | ||
|  | ```js | ||
|  | store.certificates.checkKeypair = function (opts) { | ||
|  |   console.log('certificates.checkKeypair:', opts); | ||
|  | 
 | ||
|  |   var id = opts.certificate.kid || opts.certificate.id || opts.subject; | ||
|  |   var keyblob = cache.certificateKeypairs[id]; | ||
|  | 
 | ||
|  |   if (!keyblob) { return null; } | ||
|  | 
 | ||
|  |   return JSON.parse(keyblob); | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | ### 7. Implement a method to save certificates
 | ||
|  | 
 | ||
|  | Whenever the ACME process completes successfully, you get a shiny new certificate with all of the domains you requested. | ||
|  | 
 | ||
|  | It's a good idea to save them - otherwise you run the risk of running up your rate limit and getting blocked | ||
|  | as your server restarts, respawns, auto-scales, etc. | ||
|  | 
 | ||
|  | ```js | ||
|  | store.certificates.set = function (opts) { | ||
|  |   console.log('certificates.set:', opts); | ||
|  | 
 | ||
|  |   var id = opts.certificate.id || opts.subject; | ||
|  |   var pems = opts.pems; | ||
|  |   cache.certificates[id] = JSON.stringify({ | ||
|  |     cert: pems.cert | ||
|  |   , chain: pems.chain | ||
|  |   , subject: pems.subject | ||
|  |   , altnames: pems.altnames | ||
|  |   , issuedAt: pems.issuedAt   // a.k.a. NotBefore | ||
|  |   , expiresAt: pems.expiresAt // a.k.a. NotAfter | ||
|  |   }); | ||
|  | 
 | ||
|  |   return null; | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | Note that `chain` is likely to be the same for all certificates issued by a service, | ||
|  | but there's no guarantee. The service may rotate which keys do the signing, for example. | ||
|  | 
 | ||
|  | ### 8. Implement a method to get certificates
 | ||
|  | 
 | ||
|  | Lastly, you just need a way to fetch the result of all the work you've done. | ||
|  | 
 | ||
|  | ```js | ||
|  | store.certificates.check = function (opts) { | ||
|  |   console.log('certificates.check:', opts); | ||
|  | 
 | ||
|  |   var id = opts.certificate.id || opts.subject; | ||
|  |   var certblob = cache.certificates[id]; | ||
|  | 
 | ||
|  |   if (!certblob) { return null; } | ||
|  | 
 | ||
|  |   return JSON.parse(certblob); | ||
|  | }; | ||
|  | ``` | ||
|  | 
 | ||
|  | # Huzzah!
 | ||
|  | 
 | ||
|  | There you go - you basically just have 8 setter and getter functions that usually act as dumb storage, | ||
|  | but that you can tweak with custom options if you need to. | ||
|  | 
 | ||
|  | Remember: Keep It Stupid-Simple | ||
|  | 
 | ||
|  | :D |