AJ ONeal
6 years ago
commit
4c4eaa83b7
3 changed files with 225 additions and 0 deletions
@ -0,0 +1,10 @@ |
|||
acme-v2.js |
|||
========== |
|||
|
|||
A framework for building letsencrypt clients (and other ACME v2 clients), forked from `le-acme-core.js`. |
|||
|
|||
In progress |
|||
|
|||
* get directory |
|||
* get nonce |
|||
* create account |
@ -0,0 +1,187 @@ |
|||
/*! |
|||
* acme-v2.js |
|||
* Copyright(c) 2018 AJ ONeal <aj@ppl.family> https://ppl.family
|
|||
* Apache-2.0 OR MIT (and hence also MPL 2.0) |
|||
*/ |
|||
'use strict'; |
|||
|
|||
var defaults = { |
|||
productionServerUrl: 'https://acme-v02.api.letsencrypt.org/directory' |
|||
, stagingServerUrl: 'https://acme-staging-v02.api.letsencrypt.org/directory' |
|||
, acmeChallengePrefix: '/.well-known/acme-challenge/' |
|||
, knownEndpoints: [ 'keyChange', 'meta', 'newAccount', 'newNonce', 'newOrder', 'revokeCert' ] |
|||
, challengeType: 'http-01' // dns-01
|
|||
, keyType: 'rsa' // ecdsa
|
|||
, keySize: 2048 // 256
|
|||
}; |
|||
|
|||
function create(deps) { |
|||
if (!deps) { deps = {}; } |
|||
deps.LeCore = {}; |
|||
deps.pkg = deps.pkg || require('./package.json'); |
|||
deps.os = deps.os || require('os'); |
|||
deps.process = deps.process || require('process'); |
|||
|
|||
var uaDefaults = { |
|||
pkg: "Greenlock/" + deps.pkg.version |
|||
, os: " (" + deps.os.type() + "; " + deps.process.arch + " " + deps.os.platform() + " " + deps.os.release() + ")" |
|||
, node: " Node.js/" + deps.process.version |
|||
, user: '' |
|||
}; |
|||
//var currentUAProps;
|
|||
|
|||
function getUaString() { |
|||
var userAgent = ''; |
|||
|
|||
//Object.keys(currentUAProps)
|
|||
Object.keys(uaDefaults).forEach(function (key) { |
|||
userAgent += uaDefaults[key]; |
|||
//userAgent += currentUAProps[key];
|
|||
}); |
|||
|
|||
return userAgent.trim(); |
|||
} |
|||
|
|||
function getRequest(opts) { |
|||
if (!opts) { opts = {}; } |
|||
|
|||
return deps.request.defaults({ |
|||
headers: { |
|||
'User-Agent': opts.userAgent || getUaString() |
|||
} |
|||
}); |
|||
} |
|||
|
|||
deps.request = deps.request || require('request'); |
|||
deps.promisify = deps.promisify || require('util').promisify; |
|||
|
|||
var directoryUrl = deps.directoryUrl || defaults.stagingServerUrl; |
|||
var request = deps.promisify(getRequest({})); |
|||
|
|||
var acme2 = { |
|||
getAcmeUrls: function () { |
|||
var me = this; |
|||
return request({ url: directoryUrl }).then(function (resp) { |
|||
me._directoryUrls = JSON.parse(resp.body); |
|||
me._tos = me._directoryUrls.meta.termsOfService; |
|||
return me._directoryUrls; |
|||
}); |
|||
} |
|||
, getNonce: function () { |
|||
var me = this; |
|||
return request({ method: 'HEAD', url: me._directoryUrls.newNonce }).then(function (resp) { |
|||
me._nonce = resp.toJSON().headers['replay-nonce']; |
|||
return me._nonce; |
|||
}); |
|||
} |
|||
// ACME RFC Section 7.3 Account Creation
|
|||
/* |
|||
{ |
|||
"protected": base64url({ |
|||
"alg": "ES256", |
|||
"jwk": {...}, |
|||
"nonce": "6S8IqOGY7eL2lsGoTZYifg", |
|||
"url": "https://example.com/acme/new-account" |
|||
}), |
|||
"payload": base64url({ |
|||
"termsOfServiceAgreed": true, |
|||
"onlyReturnExisting": false, |
|||
"contact": [ |
|||
"mailto:cert-admin@example.com", |
|||
"mailto:admin@example.com" |
|||
] |
|||
}), |
|||
"signature": "RZPOnYoPs1PhjszF...-nh6X1qtOFPB519I" |
|||
} |
|||
*/ |
|||
, registerNewAccount: function () { |
|||
var me = this; |
|||
var RSA = require('rsa-compat').RSA; |
|||
var crypto = require('crypto'); |
|||
RSA.signJws = RSA.generateJws = RSA.generateSignatureJws = RSA.generateSignatureJwk = |
|||
function (keypair, payload, nonce) { |
|||
var prot = {}; |
|||
if (nonce) { |
|||
if ('string' === typeof nonce) { |
|||
prot.nonce = nonce; |
|||
} else { |
|||
prot = nonce; |
|||
} |
|||
} |
|||
keypair = RSA._internal.import(keypair); |
|||
keypair = RSA._internal.importForge(keypair); |
|||
keypair.publicKeyJwk = RSA.exportPublicJwk(keypair); |
|||
|
|||
// Compute JWS signature
|
|||
var protectedHeader = ""; |
|||
if (Object.keys(prot).length) { |
|||
protectedHeader = JSON.stringify(prot); // { alg: prot.alg, nonce: prot.nonce, url: prot.url });
|
|||
} |
|||
var protected64 = RSA.utils.toWebsafeBase64(new Buffer(protectedHeader).toString('base64')); |
|||
var payload64 = RSA.utils.toWebsafeBase64(payload.toString('base64')); |
|||
var raw = protected64 + "." + payload64; |
|||
var sha256Buf = crypto.createHash('sha256').update(raw).digest(); |
|||
var sig64; |
|||
|
|||
if (RSA._URSA) { |
|||
sig64 = RSA._ursaGenerateSig(keypair, sha256Buf); |
|||
} else { |
|||
sig64 = RSA._forgeGenerateSig(keypair, sha256Buf); |
|||
} |
|||
|
|||
return { |
|||
/* |
|||
header: { |
|||
alg: "RS256" |
|||
, jwk: keypair.publicKeyJwk |
|||
} |
|||
*/ |
|||
protected: protected64 |
|||
, payload: payload64 |
|||
, signature: sig64 |
|||
}; |
|||
}; |
|||
|
|||
var options = { |
|||
email: 'coolaj86@gmail.com' |
|||
, keypair: RSA.import({ privateKeyPem: require('fs').readFileSync(__dirname + '/privkey.pem') }) |
|||
}; |
|||
var body = { |
|||
termsOfServiceAgreed: true |
|||
, onlyReturnExisting: false |
|||
, contact: [ 'mailto:' + options.email ] |
|||
}; |
|||
var payload = JSON.stringify(body, null, 2); |
|||
var jws = RSA.signJws( |
|||
options.keypair |
|||
, new Buffer(payload) |
|||
, { nonce: me._nonce, alg: 'RS256', url: me._directoryUrls.newAccount, jwk: RSA.exportPublicJwk(options.keypair) } |
|||
); |
|||
|
|||
console.log('jws:'); |
|||
console.log(jws); |
|||
return request({ |
|||
method: 'POST' |
|||
, url: me._directoryUrls.newAccount |
|||
, headers: { 'Content-Type': 'application/jose+json' } |
|||
, json: jws |
|||
}).then(function (resp) { |
|||
me._nonce = resp.toJSON().headers['replay-nonce']; |
|||
console.log(resp.toJSON()); |
|||
return resp.body; |
|||
}); |
|||
} |
|||
}; |
|||
return acme2; |
|||
} |
|||
|
|||
var acme2 = create(); |
|||
acme2.getAcmeUrls().then(function (body) { |
|||
console.log(body); |
|||
acme2.getNonce().then(function (nonce) { |
|||
console.log(nonce); |
|||
acme2.registerNewAccount().then(function (account) { |
|||
console.log(account); |
|||
}); |
|||
}); |
|||
}); |
@ -0,0 +1,28 @@ |
|||
{ |
|||
"name": "acme-v2", |
|||
"version": "1.0.0", |
|||
"description": "A framework for building letsencrypt clients (and other ACME v2 clients), forked from le-acme-core.js.", |
|||
"main": "node.js", |
|||
"scripts": { |
|||
"test": "echo \"Error: no test specified\" && exit 1" |
|||
}, |
|||
"repository": { |
|||
"type": "git", |
|||
"url": "ssh://gitea@git.coolaj86.com:22042/coolaj86/acme-v2.js.git" |
|||
}, |
|||
"keywords": [ |
|||
"acmev2", |
|||
"acme-v2", |
|||
"acme", |
|||
"letsencrypt-v2", |
|||
"letsencryptv2", |
|||
"greenlock", |
|||
"greenlock2" |
|||
], |
|||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)", |
|||
"license": "(MIT OR Apache-2.0)", |
|||
"dependencies": { |
|||
"request": "^2.85.0", |
|||
"rsa-compat": "^1.2.7" |
|||
} |
|||
} |
Loading…
Reference in new issue