AJ ONeal
5 years ago
commit
cbe20c4d00
6 changed files with 525 additions and 0 deletions
@ -0,0 +1,61 @@ |
|||
# ---> Node |
|||
# Logs |
|||
logs |
|||
*.log |
|||
npm-debug.log* |
|||
yarn-debug.log* |
|||
yarn-error.log* |
|||
|
|||
# Runtime data |
|||
pids |
|||
*.pid |
|||
*.seed |
|||
*.pid.lock |
|||
|
|||
# Directory for instrumented libs generated by jscoverage/JSCover |
|||
lib-cov |
|||
|
|||
# Coverage directory used by tools like istanbul |
|||
coverage |
|||
|
|||
# nyc test coverage |
|||
.nyc_output |
|||
|
|||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) |
|||
.grunt |
|||
|
|||
# Bower dependency directory (https://bower.io/) |
|||
bower_components |
|||
|
|||
# node-waf configuration |
|||
.lock-wscript |
|||
|
|||
# Compiled binary addons (http://nodejs.org/api/addons.html) |
|||
build/Release |
|||
|
|||
# Dependency directories |
|||
node_modules/ |
|||
jspm_packages/ |
|||
|
|||
# Typescript v1 declaration files |
|||
typings/ |
|||
|
|||
# Optional npm cache directory |
|||
.npm |
|||
|
|||
# Optional eslint cache |
|||
.eslintcache |
|||
|
|||
# Optional REPL history |
|||
.node_repl_history |
|||
|
|||
# Output of 'npm pack' |
|||
*.tgz |
|||
|
|||
# Yarn Integrity file |
|||
.yarn-integrity |
|||
|
|||
# dotenv environment variables file |
|||
.env |
|||
|
|||
|
@ -0,0 +1,41 @@ |
|||
Copyright 2019 AJ ONeal |
|||
|
|||
This is open source software; you can redistribute it and/or modify it under the |
|||
terms of either: |
|||
|
|||
a) the "MIT License" |
|||
b) the "Apache-2.0 License" |
|||
|
|||
MIT License |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
|
|||
Apache-2.0 License Summary |
|||
|
|||
Licensed under the Apache License, Version 2.0 (the "License"); |
|||
you may not use this file except in compliance with the License. |
|||
You may obtain a copy of the License at |
|||
|
|||
http://www.apache.org/licenses/LICENSE-2.0 |
|||
|
|||
Unless required by applicable law or agreed to in writing, software |
|||
distributed under the License is distributed on an "AS IS" BASIS, |
|||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|||
See the License for the specific language governing permissions and |
|||
limitations under the License. |
@ -0,0 +1,245 @@ |
|||
# 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 |
@ -0,0 +1,144 @@ |
|||
'use strict'; |
|||
|
|||
module.exports.create = function (opts) { |
|||
// pass in database url, connection string, filepath,
|
|||
// or whatever it is you need to get your job done well
|
|||
|
|||
|
|||
|
|||
// This is our in-memory storage.
|
|||
// We take it from the outside to make testing the dummy module easier.
|
|||
var cache = opts.cache || {}; |
|||
if (!cache.accounts) { cache.accounts = {}; } |
|||
if (!cache.certificates) { cache.certificates = {}; } |
|||
// Although we could have two collections of keypairs,
|
|||
// it's also fine to store both types together.
|
|||
if (!cache.keypairs) { cache.keypairs = {}; } |
|||
|
|||
|
|||
|
|||
var store = {}; |
|||
// any options you need per instance
|
|||
// (probably okay to leave empty)
|
|||
store.options = {}; |
|||
store.accounts = {}; |
|||
store.certificates = {}; |
|||
|
|||
|
|||
|
|||
// Whenever a new keypair is used to successfully create an account, we need to save its keypair
|
|||
store.accounts.setKeypair = function (opts) { |
|||
console.log('accounts.setKeypair:', opts); |
|||
|
|||
var id = opts.account.id || opts.email || 'default'; |
|||
var keypair = opts.keypair; |
|||
|
|||
cache.keypairs[id] = JSON.stringify({ |
|||
privateKeyPem: keypair.privateKeyPem |
|||
, privateKeyJwk: keypair.privateKeyJwk |
|||
}); |
|||
|
|||
return null; // or Promise.resolve(null);
|
|||
}; |
|||
|
|||
|
|||
|
|||
// We need a way to retrieve a prior account's keypair for renewals and additional ACME certificate "orders"
|
|||
store.accounts.checkKeypair = function (opts) { |
|||
console.log('accounts.checkKeypair:', opts); |
|||
|
|||
var id = opts.account.id || opts.email || 'default'; |
|||
var keyblob = cache.keypairs[id]; |
|||
|
|||
if (!keyblob) { return null; } |
|||
|
|||
return JSON.parse(keyblob); |
|||
}; |
|||
|
|||
|
|||
|
|||
// We can optionally implement ACME account storage and retrieval
|
|||
// (to reduce API calls), but it's really not necessary.
|
|||
/* |
|||
store.accounts.set = function (opts) { |
|||
console.log('accounts.set:', opts); |
|||
return null; |
|||
}; |
|||
store.accounts.check = function (opts) { |
|||
var id = opts.account.id || opts.email || 'default'; |
|||
console.log('accounts.check:', opts); |
|||
return null; |
|||
}; |
|||
*/ |
|||
|
|||
|
|||
|
|||
// The certificate keypairs must not be the same as any account keypair
|
|||
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.keypairs[id] = JSON.stringify({ |
|||
privateKeyPem: keypair.privateKeyPem |
|||
, privateKeyJwk: keypair.privateKeyJwk |
|||
}); |
|||
// Note: you can use the "keypairs" package to convert between
|
|||
// public and private for jwk and pem, as well as convert JWK <-> PEM
|
|||
|
|||
return null; |
|||
}; |
|||
|
|||
|
|||
|
|||
// You won't be able to use a certificate without it's private key, gotta save it
|
|||
store.certificates.checkKeypair = function (opts) { |
|||
console.log('certificates.checkKeypair:', opts); |
|||
|
|||
var id = opts.certificate.kid || opts.certificate.id || opts.subject; |
|||
var keyblob = cache.keypairs[id]; |
|||
|
|||
if (!keyblob) { return null; } |
|||
|
|||
return JSON.parse(keyblob); |
|||
}; |
|||
|
|||
|
|||
|
|||
// And you'll also need to save certificates. You may find the metadata useful to save
|
|||
// (perhaps to delete expired keys), but the same information can also be redireved from
|
|||
// the key using the "cert-info" package.
|
|||
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; |
|||
}; |
|||
|
|||
|
|||
|
|||
// This is actually the first thing to be called after approveDomins(),
|
|||
// but it's easiest to implement last since it's not useful until there
|
|||
// are certs that can actually be loaded from storage.
|
|||
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); |
|||
}; |
|||
}; |
@ -0,0 +1,5 @@ |
|||
{ |
|||
"name": "le-store-memory", |
|||
"version": "1.0.0", |
|||
"lockfileVersion": 1 |
|||
} |
@ -0,0 +1,29 @@ |
|||
{ |
|||
"name": "le-store-memory", |
|||
"version": "1.0.0", |
|||
"description": "An in-memory reference implementation for account, certificate, and keypair storage strategies in Greenlock", |
|||
"homepage": "https://git.coolaj86.com/coolaj86/le-store-memory.js", |
|||
"main": "index.js", |
|||
"directories": { |
|||
"test": "tests" |
|||
}, |
|||
"scripts": { |
|||
"test": "node tests" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "https://git.coolaj86.com/coolaj86/le-store-memory.js.git" |
|||
}, |
|||
"keywords": [ |
|||
"greenlock", |
|||
"acme", |
|||
"json", |
|||
"keypairs", |
|||
"certificates", |
|||
"store", |
|||
"database" |
|||
], |
|||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |
|||
"license": "MPL-2.0", |
|||
"dependencies": {} |
|||
} |
Loading…
Reference in new issue