Simple walkthrough node.js script
This commit is contained in:
parent
f5546ad407
commit
bc583a0df3
|
|
@ -0,0 +1,33 @@
|
|||
# A JS tester for boulder
|
||||
|
||||
The node.js scripts in this directory provide a simple end-to-end test of Boulder. (Using some pieces from [node-acme](https://github.com/letsencrypt/node-acme/)) To run:
|
||||
|
||||
```
|
||||
# Install dependencies
|
||||
> npm install inquirer cli node-forge
|
||||
|
||||
# Start cfssl with signing parameters
|
||||
# (These are the default parameters to use a Yubikey.)
|
||||
# (You'll need to make your own key, cert, and policy.)
|
||||
> go install -tags pkcs11 github.com/cloudflare/cfssl/cmd/cfssl
|
||||
> cfssl serve -port 8888 -ca ca.cert.pem \
|
||||
-pkcs11-module "/Library/OpenSC/lib/opensc-pkcs11.so" \
|
||||
-pkcs11-token "Yubico Yubik NEO CCID" \
|
||||
-pkcs11-pin 123456 \
|
||||
-pkcs11-label "PIV AUTH key" \
|
||||
-config policy.json
|
||||
|
||||
# Start boulder
|
||||
# (Change CFSSL parameters to match your setup.)
|
||||
> go install github.com/letsencrypt/boulder
|
||||
> boulder-start --cfssl localhost:8888
|
||||
--cfsslProfile ee \
|
||||
--cfsslAuthKey 79999d86250c367a2b517a1ae7d409c1 \
|
||||
monolithic
|
||||
|
||||
# Client side
|
||||
> mkdir -p .well-known/acme-challenge/
|
||||
> node demo.js
|
||||
> mv -- *.txt .well-known/acme-challenge/ # In a different window
|
||||
> python -m SimpleHTTPServer 5001 # In yet another window
|
||||
```
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
module.exports = {
|
||||
|
||||
fromStandardB64: function(x) {
|
||||
return x.replace(/[+]/g, "-").replace(/\//g, "_").replace(/=/g,"");
|
||||
},
|
||||
|
||||
toStandardB64: function(x) {
|
||||
var b64 = x.replace(/-/g, "+").replace(/_/g, "/").replace(/=/g, "");
|
||||
|
||||
switch (b64.length % 4) {
|
||||
case 2: b64 += "=="; break;
|
||||
case 3: b64 += "="; break;
|
||||
}
|
||||
|
||||
return b64;
|
||||
},
|
||||
|
||||
b64enc: function(buffer) {
|
||||
return this.fromStandardB64(buffer.toString("base64"));
|
||||
},
|
||||
|
||||
b64dec: function(str) {
|
||||
return new Buffer(this.toStandardB64(str), "base64");
|
||||
},
|
||||
|
||||
isB64String: function(x) {
|
||||
return (typeof(x) == "string") && !x.match(/[^a-zA-Z0-9_-]/);
|
||||
},
|
||||
|
||||
fieldsPresent: function(fields, object) {
|
||||
for (var i in fields) {
|
||||
if (!(fields[i] in object)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
validSignature: function(sig) {
|
||||
return ((typeof(sig) == "object") &&
|
||||
("alg" in sig) && (typeof(sig.alg) == "string") &&
|
||||
("nonce" in sig) && this.isB64String(sig.nonce) &&
|
||||
("sig" in sig) && this.isB64String(sig.sig) &&
|
||||
("jwk" in sig) && this.validJWK(sig.jwk));
|
||||
},
|
||||
|
||||
validJWK: function(jwk) {
|
||||
return ((typeof(jwk) == "object") && ("kty" in jwk) && (
|
||||
((jwk.kty == "RSA")
|
||||
&& ("n" in jwk) && this.isB64String(jwk.n)
|
||||
&& ("e" in jwk) && this.isB64String(jwk.e)) ||
|
||||
((jwk.kty == "EC")
|
||||
&& ("crv" in jwk)
|
||||
&& ("x" in jwk) && this.isB64String(jwk.x)
|
||||
&& ("y" in jwk) && this.isB64String(jwk.y))
|
||||
) && !("d" in jwk));
|
||||
},
|
||||
|
||||
// A simple, non-standard fingerprint for a JWK,
|
||||
// just so that we don't have to store objects
|
||||
keyFingerprint: function(jwk) {
|
||||
switch (jwk.kty) {
|
||||
case "RSA": return jwk.n;
|
||||
case "EC": return jwk.crv + jwk.x + jwk.y;
|
||||
}
|
||||
throw "Unrecognized key type";
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
var crypto = require("crypto");
|
||||
var forge = require("node-forge");
|
||||
var util = require("./acme-util.js");
|
||||
|
||||
var TOKEN_SIZE = 16;
|
||||
var NONCE_SIZE = 16;
|
||||
|
||||
function bytesToBuffer(bytes) {
|
||||
return new Buffer(forge.util.bytesToHex(bytes), "hex");
|
||||
}
|
||||
|
||||
function bufferToBytes(buf) {
|
||||
return forge.util.hexToBytes(buf.toString("hex"));
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
return util.b64enc(bytesToBuffer(bytes));
|
||||
}
|
||||
|
||||
function base64ToBytes(base64) {
|
||||
return bufferToBytes(util.b64dec(base64));
|
||||
}
|
||||
|
||||
function bnToBase64(bn) {
|
||||
var hex = bn.toString(16);
|
||||
if (hex.length % 2 == 1) { hex = "0" + hex; }
|
||||
return util.b64enc(new Buffer(hex, "hex"));
|
||||
}
|
||||
|
||||
function base64ToBn(base64) {
|
||||
return new forge.jsbn.BigInteger(util.b64dec(base64).toString("hex"), 16);
|
||||
}
|
||||
|
||||
function importPrivateKey(privateKey) {
|
||||
return forge.pki.rsa.setPrivateKey(
|
||||
base64ToBn(privateKey.n),
|
||||
base64ToBn(privateKey.e), base64ToBn(privateKey.d),
|
||||
base64ToBn(privateKey.p), base64ToBn(privateKey.q),
|
||||
base64ToBn(privateKey.dp),base64ToBn(privateKey.dq),
|
||||
base64ToBn(privateKey.qi));
|
||||
}
|
||||
|
||||
function importPublicKey(publicKey) {
|
||||
return forge.pki.rsa.setPublicKey(
|
||||
base64ToBn(publicKey.n),
|
||||
base64ToBn(publicKey.e));
|
||||
}
|
||||
|
||||
function exportPrivateKey(privateKey) {
|
||||
return {
|
||||
"kty": "RSA",
|
||||
"n": bnToBase64(privateKey.n),
|
||||
"e": bnToBase64(privateKey.e),
|
||||
"d": bnToBase64(privateKey.d),
|
||||
"p": bnToBase64(privateKey.p),
|
||||
"q": bnToBase64(privateKey.q),
|
||||
"dp": bnToBase64(privateKey.dP),
|
||||
"dq": bnToBase64(privateKey.dQ),
|
||||
"qi": bnToBase64(privateKey.qInv)
|
||||
};
|
||||
}
|
||||
|
||||
function exportPublicKey(publicKey) {
|
||||
return {
|
||||
"kty": "RSA",
|
||||
"n": bnToBase64(publicKey.n),
|
||||
"e": bnToBase64(publicKey.e)
|
||||
};
|
||||
}
|
||||
|
||||
// A note on formats:
|
||||
// * Keys are always represented as JWKs
|
||||
// * Signature objects are in ACME format
|
||||
// * Certs and CSRs are base64-encoded
|
||||
module.exports = {
|
||||
///// RANDOM STRINGS
|
||||
|
||||
randomString: function(nBytes) {
|
||||
return bytesToBase64(forge.random.getBytesSync(nBytes));
|
||||
},
|
||||
|
||||
randomSerialNumber: function() {
|
||||
return forge.util.bytesToHex(forge.random.getBytesSync(4));
|
||||
},
|
||||
|
||||
newToken: function() {
|
||||
return this.randomString(TOKEN_SIZE);
|
||||
},
|
||||
|
||||
///// SHA-256
|
||||
|
||||
sha256: function(buf) {
|
||||
return crypto.createHash('sha256').update(buf).digest('hex');
|
||||
},
|
||||
|
||||
///// KEY PAIR MANAGEMENT
|
||||
|
||||
generateKeyPair: function(bits) {
|
||||
var keyPair = forge.pki.rsa.generateKeyPair({bits: bits, e: 0x10001});
|
||||
return {
|
||||
privateKey: exportPrivateKey(keyPair.privateKey),
|
||||
publicKey: exportPublicKey(keyPair.publicKey)
|
||||
};
|
||||
},
|
||||
|
||||
importPemPrivateKey: function(pem) {
|
||||
var key = forge.pki.privateKeyFromPem(pem);
|
||||
return {
|
||||
privateKey: exportPrivateKey(key),
|
||||
publicKey: exportPublicKey(key)
|
||||
};
|
||||
},
|
||||
|
||||
importPemCertificate: function(pem) {
|
||||
return forge.pki.certificateFromPem(pem);
|
||||
},
|
||||
|
||||
privateKeyToPem: function(privateKey) {
|
||||
var priv = importPrivateKey(privateKey);
|
||||
return forge.pki.privateKeyToPem(priv);
|
||||
},
|
||||
|
||||
certificateToPem: function(certificate) {
|
||||
var derCert = base64ToBytes(certificate);
|
||||
var cert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
||||
return forge.pki.certificateToPem(cert);
|
||||
},
|
||||
|
||||
certificateRequestToPem: function(csr) {
|
||||
var derReq = base64ToBytes(csr);
|
||||
var c = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derReq));
|
||||
return forge.pki.certificateRequestToPem(c);
|
||||
},
|
||||
|
||||
///// SIGNATURE GENERATION / VERIFICATION
|
||||
|
||||
generateSignature: function(keyPair, payload) {
|
||||
var nonce = bytesToBuffer(forge.random.getBytesSync(NONCE_SIZE));
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
|
||||
// Compute JWS signature
|
||||
var protectedHeader = JSON.stringify({
|
||||
nonce: util.b64enc(nonce)
|
||||
});
|
||||
var protected64 = util.b64enc(new Buffer(protectedHeader));
|
||||
var payload64 = util.b64enc(payload);
|
||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
||||
var md = forge.md.sha256.create();
|
||||
md.update(signatureInput);
|
||||
var sig = privateKey.sign(md);
|
||||
|
||||
return {
|
||||
header: {
|
||||
alg: "RS256",
|
||||
jwk: keyPair.publicKey,
|
||||
},
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: util.b64enc(bytesToBuffer(sig)),
|
||||
}
|
||||
},
|
||||
|
||||
verifySignature: function(jws) {
|
||||
if (jws.protected) {
|
||||
if (!jws.header) {
|
||||
jws.header = {};
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(jws.protected);
|
||||
var protectedJSON = util.b64dec(jws.protected).toString();
|
||||
console.log(protectedJSON);
|
||||
var protectedObj = JSON.parse(protectedJSON);
|
||||
for (key in protectedObj) {
|
||||
jws.header[key] = protectedObj[key];
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("error unmarshaling json: "+e)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes validSignature(sig)
|
||||
if (!jws.header.jwk || (jws.header.jwk.kty != "RSA")) {
|
||||
// Unsupported key type
|
||||
console.log("Unsupported key type");
|
||||
return false;
|
||||
} else if (!jws.header.alg || !jws.header.alg.match(/^RS/)) {
|
||||
// Unsupported algorithm
|
||||
console.log("Unsupported alg: "+jws.header.alg);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compute signature input
|
||||
var protected64 = (jws.protected)? jws.protected : "";
|
||||
var payload64 = (jws.payload)? jws.payload : "";
|
||||
var signatureInputBuf = new Buffer(protected64 + "." + payload64);
|
||||
var signatureInput = bufferToBytes(signatureInputBuf);
|
||||
|
||||
// Compute message digest
|
||||
var md;
|
||||
switch (jws.header.alg) {
|
||||
case "RS1": md = forge.md.sha1.create(); break;
|
||||
case "RS256": md = forge.md.sha256.create(); break;
|
||||
case "RS384": md = forge.md.sha384.create(); break;
|
||||
case "RS512": md = forge.md.sha512.create(); break;
|
||||
default: return false; // Unsupported algorithm
|
||||
}
|
||||
md.update(signatureInput);
|
||||
|
||||
// Import the key and signature
|
||||
var publicKey = importPublicKey(jws.header.jwk);
|
||||
var sig = bufferToBytes(util.b64dec(jws.signature));
|
||||
|
||||
return publicKey.verify(md.digest().bytes(), sig);
|
||||
},
|
||||
|
||||
///// CSR GENERATION / VERIFICATION
|
||||
|
||||
generateCSR: function(keyPair, identifier) {
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
var publicKey = importPublicKey(keyPair.publicKey);
|
||||
|
||||
// Create and sign the CSR
|
||||
var csr = forge.pki.createCertificationRequest();
|
||||
csr.publicKey = publicKey;
|
||||
csr.setSubject([{ name: 'commonName', value: identifier }]);
|
||||
csr.sign(privateKey);
|
||||
|
||||
// Convert CSR -> DER -> Base64
|
||||
var der = forge.asn1.toDer(forge.pki.certificationRequestToAsn1(csr));
|
||||
return util.b64enc(bytesToBuffer(der));
|
||||
},
|
||||
|
||||
verifiedCommonName: function(csr_b64) {
|
||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
||||
|
||||
if (!csr.verify()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
||||
if (csr.subject.attributes[i].name == "commonName") {
|
||||
return csr.subject.attributes[i].value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
///// CERTIFICATE GENERATION
|
||||
|
||||
// 'ca' parameter includes information about the CA
|
||||
// {
|
||||
// distinguishedName: /* forge-formatted DN */
|
||||
// keyPair: {
|
||||
// publicKey: /* JWK */
|
||||
// privateKey: /* JWK */
|
||||
// }
|
||||
// }
|
||||
generateCertificate: function(ca, serialNumber, csr_b64) {
|
||||
var der = bufferToBytes(util.b64dec(csr_b64));
|
||||
var csr = forge.pki.certificationRequestFromAsn1(forge.asn1.fromDer(der));
|
||||
|
||||
// Extract the public key and common name
|
||||
var publicKey = csr.publicKey;
|
||||
var commonName = null;
|
||||
for (var i=0; i<csr.subject.attributes.length; ++i) {
|
||||
if (csr.subject.attributes[i].name == "commonName") {
|
||||
commonName = csr.subject.attributes[i].value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!commonName) { return false; }
|
||||
|
||||
// Create the certificate
|
||||
var cert = forge.pki.createCertificate();
|
||||
cert.publicKey = publicKey;
|
||||
cert.serialNumber = serialNumber;
|
||||
|
||||
// 1-year validity
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
|
||||
cert.setSubject([{ name: "commonName", value: commonName }]);
|
||||
cert.setIssuer(ca.distinguishedName);
|
||||
cert.setExtensions([
|
||||
{ name: "basicConstraints", cA: false },
|
||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
||||
{ name: "extKeyUsage", serverAuth: true },
|
||||
{ name: "subjectAltName", altNames: [{ type: 2, value: commonName }] }
|
||||
]);
|
||||
|
||||
// Import signing key and sign
|
||||
var privateKey = importPrivateKey(ca.keyPair.privateKey);
|
||||
cert.sign(privateKey);
|
||||
|
||||
// Return base64-encoded DER
|
||||
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
||||
return bytesToBuffer(der);
|
||||
},
|
||||
|
||||
generateDvsniCertificate: function(keyPair, nonceName, zName) {
|
||||
var cert = forge.pki.createCertificate();
|
||||
cert.publicKey = importPublicKey(keyPair.publicKey);
|
||||
cert.serialNumber = '01';
|
||||
cert.validity.notBefore = new Date();
|
||||
cert.validity.notAfter = new Date();
|
||||
cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
|
||||
cert.setSubject([{ name: "commonName", value: nonceName }]);
|
||||
cert.setIssuer([{ name: "commonName", value: nonceName }]);
|
||||
cert.setExtensions([
|
||||
{ name: "basicConstraints", cA: false },
|
||||
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
||||
{ name: "extKeyUsage", serverAuth: true },
|
||||
{ name: "subjectAltName", altNames: [
|
||||
{ type: 2, value: nonceName },
|
||||
{ type: 2, value: zName }
|
||||
]}
|
||||
]);
|
||||
cert.sign(importPrivateKey(keyPair.privateKey));
|
||||
|
||||
// Return base64-encoded DER, as above
|
||||
var der = forge.asn1.toDer(forge.pki.certificateToAsn1(cert));
|
||||
return util.b64enc(bytesToBuffer(der));
|
||||
},
|
||||
|
||||
///// TLS CONTEXT GENERATION
|
||||
|
||||
createContext: function(keyPair, cert) {
|
||||
var privateKey = importPrivateKey(keyPair.privateKey);
|
||||
var derCert = bufferToBytes(util.b64dec(cert));
|
||||
var realCert = forge.pki.certificateFromAsn1(forge.asn1.fromDer(derCert));
|
||||
return crypto.createCredentials({
|
||||
key: forge.pki.privateKeyToPem(privateKey),
|
||||
cert: forge.pki.certificateToPem(realCert)
|
||||
}).context;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
"use strict";
|
||||
|
||||
var inquirer = require("inquirer");
|
||||
var cli = require("cli");
|
||||
var http = require('http');
|
||||
var fs = require('fs');
|
||||
var url = require('url');
|
||||
var util = require("./acme-util");
|
||||
var crypto = require("./crypto-util");
|
||||
|
||||
var questions = {
|
||||
email: [{
|
||||
type: "input",
|
||||
name: "email",
|
||||
message: "Please enter your email address (for recovery purposes)",
|
||||
validate: function(value) {
|
||||
var pass = value.match(/[\w.+-]+@[\w.-]+/i);
|
||||
if (pass) {
|
||||
return true;
|
||||
} else {
|
||||
return "Please enter a valid email address";
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
terms: [{
|
||||
type: "confirm",
|
||||
name: "terms",
|
||||
message: "Do you agree to these terms?",
|
||||
default: false
|
||||
}],
|
||||
|
||||
domain: [{
|
||||
type: "input",
|
||||
name: "domain",
|
||||
message: "Please enter the domain name for the certificate",
|
||||
validate: function(value) {
|
||||
var pass = value.match(/[\w.-]+/i);
|
||||
if (pass) {
|
||||
return true;
|
||||
} else {
|
||||
return "Please enter a valid domain name";
|
||||
}
|
||||
}
|
||||
}],
|
||||
|
||||
readyToValidate: [{
|
||||
type: "input",
|
||||
name: "noop",
|
||||
message: "Press enter to when you're ready to proceed",
|
||||
}],
|
||||
|
||||
files: [{
|
||||
type: "input",
|
||||
name: "keyFile",
|
||||
message: "Name for key file",
|
||||
default: "key.pem"
|
||||
},{
|
||||
type: "input",
|
||||
name: "certFile",
|
||||
message: "Name for certificate file",
|
||||
default: "cert.pem"
|
||||
}],
|
||||
};
|
||||
|
||||
var state = {
|
||||
keyPairBits: 512,
|
||||
keyPair: null,
|
||||
|
||||
newRegistrationURL: "http://localhost:4000/acme/new-reg",
|
||||
registrationURL: "",
|
||||
|
||||
termsRequired: false,
|
||||
|
||||
domain: null,
|
||||
|
||||
newAuthorizationURL: "",
|
||||
authorizationURL: "",
|
||||
responseURL: "",
|
||||
path: "",
|
||||
retryDelay: 1000,
|
||||
|
||||
newCertificateURL: "",
|
||||
certificateURL: "",
|
||||
};
|
||||
|
||||
function parseLink(link) {
|
||||
try {
|
||||
// NB: Takes last among links with the same "rel" value
|
||||
var links = link.split(',').map(function(link) {
|
||||
var parts = link.trim().split(";");
|
||||
var url = parts.shift().replace(/[<>]/g, "");
|
||||
var 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;
|
||||
}, {});
|
||||
return links;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
The asynchronous nature of node.js libraries makes the control flow a
|
||||
little hard to follow here, but it pretty much goes straight down the
|
||||
page, with detours through the `inquirer` and `http` libraries.
|
||||
|
||||
main
|
||||
|
|
||||
register
|
||||
|
|
||||
getTerms
|
||||
| \
|
||||
| getAgreement (TODO)
|
||||
| /
|
||||
getDomain
|
||||
|
|
||||
getChallenges
|
||||
|
|
||||
getReadyToValidate
|
||||
|
|
||||
sendResponse
|
||||
|
|
||||
ensureValidation
|
||||
|
|
||||
getCertificate
|
||||
|
|
||||
downloadCertificate
|
||||
|
|
||||
saveFiles
|
||||
|
||||
|
||||
*/
|
||||
|
||||
function main() {
|
||||
console.log("Generating key pair...");
|
||||
state.keyPair = crypto.generateKeyPair(state.keyPairBits);
|
||||
console.log();
|
||||
inquirer.prompt(questions.email, register)
|
||||
}
|
||||
|
||||
function register(answers) {
|
||||
var email = answers.email;
|
||||
|
||||
// Register public key
|
||||
var registerMessage = JSON.stringify({
|
||||
contact: [ "mailto:" + email ]
|
||||
});
|
||||
var jws = crypto.generateSignature(state.keyPair, new Buffer(registerMessage));
|
||||
var payload = JSON.stringify(jws);
|
||||
|
||||
var options = url.parse(state.newRegistrationURL);
|
||||
options.method = "POST";
|
||||
var req = http.request(options, getTerms);
|
||||
req.write(payload)
|
||||
req.end();
|
||||
}
|
||||
|
||||
function getTerms(resp) {
|
||||
if (Math.floor(resp.statusCode / 100) != 2) {
|
||||
// Non-2XX response
|
||||
console.log("Registration request failed with code " + resp.statusCode);
|
||||
return;
|
||||
}
|
||||
|
||||
var links = parseLink(resp.headers["link"]);
|
||||
if (!links || !("next" in links)) {
|
||||
console.log("The server did not provide information to proceed");
|
||||
return
|
||||
}
|
||||
|
||||
state.registrationURL = resp.headers["location"];
|
||||
state.newAuthorizationURL = links["next"];
|
||||
state.termsRequired = ("terms-of-service" in links);
|
||||
|
||||
if (state.termsRequired) {
|
||||
// TODO getAgreement
|
||||
// inquirer.prompt(questions.terms, getAgreement);
|
||||
console.log("The CA requires your agreement to terms (not supported).");
|
||||
return
|
||||
} else {
|
||||
inquirer.prompt(questions.domain, getChallenges);
|
||||
}
|
||||
}
|
||||
|
||||
function getChallenges(answers) {
|
||||
state.domain = answers.domain;
|
||||
|
||||
// Register public key
|
||||
var authzMessage = JSON.stringify({
|
||||
identifier: {
|
||||
type: "dns",
|
||||
value: state.domain
|
||||
}
|
||||
});
|
||||
var jws = crypto.generateSignature(state.keyPair, new Buffer(authzMessage));
|
||||
var payload = JSON.stringify(jws);
|
||||
|
||||
var options = url.parse(state.newAuthorizationURL);
|
||||
options.method = "POST";
|
||||
var req = http.request(options, getReadyToValidate);
|
||||
req.write(payload)
|
||||
req.end();
|
||||
}
|
||||
|
||||
function getReadyToValidate(resp) {
|
||||
if (Math.floor(resp.statusCode / 100) != 2) {
|
||||
// Non-2XX response
|
||||
console.log("Authorization request failed with code " + resp.statusCode)
|
||||
return;
|
||||
}
|
||||
|
||||
var links = parseLink(resp.headers["link"]);
|
||||
if (!links || !("next" in links)) {
|
||||
console.log("The server did not provide information to proceed");
|
||||
return
|
||||
}
|
||||
|
||||
state.authorizationURL = resp.headers["location"];
|
||||
state.newCertificateURL = links["next"];
|
||||
|
||||
var body = ""
|
||||
resp.on('data', function(chunk) {
|
||||
body += chunk;
|
||||
});
|
||||
resp.on('end', function(chunk) {
|
||||
if (chunk) { body += chunk; }
|
||||
|
||||
var authz = JSON.parse(body);
|
||||
|
||||
var simpleHttps = authz.challenges.filter(function(x) { return x.type == "simpleHttps"; });
|
||||
if (simpleHttps.length == 0) {
|
||||
console.log("The server didn't offer any challenges we can handle.");
|
||||
return;
|
||||
}
|
||||
|
||||
var challenge = simpleHttps[0];
|
||||
var path = crypto.randomString(8) + ".txt";
|
||||
fs.writeFileSync(path, challenge.token);
|
||||
state.responseURL = challenge["uri"];
|
||||
state.path = path;
|
||||
|
||||
console.log();
|
||||
console.log("To validate that you own "+ state.domain +", the CA has\n" +
|
||||
"asked you to provision a file on your server. I've saved\n" +
|
||||
"the file here for you.\n");
|
||||
console.log(" File: " + path);
|
||||
console.log(" URL: http://"+ state.domain +"/.well-known/acme-challenge/"+ path);
|
||||
console.log();
|
||||
|
||||
// To do this locally (boulder connects to port 5001)
|
||||
// > mkdir -p .well-known/acme-challenge/
|
||||
// > mv $CHALLENGE_FILE ./well-known/acme-challenge/
|
||||
// > python -m SimpleHTTPServer 5001
|
||||
|
||||
inquirer.prompt(questions.readyToValidate, sendResponse);
|
||||
});
|
||||
}
|
||||
|
||||
function sendResponse() {
|
||||
var responseMessage = JSON.stringify({
|
||||
path: state.path
|
||||
});
|
||||
var jws = crypto.generateSignature(state.keyPair, new Buffer(responseMessage));
|
||||
var payload = JSON.stringify(jws);
|
||||
|
||||
cli.spinner("Validating domain");
|
||||
|
||||
var options = url.parse(state.responseURL);
|
||||
options.method = "POST";
|
||||
var req = http.request(options, ensureValidation);
|
||||
req.write(payload)
|
||||
req.end();
|
||||
}
|
||||
|
||||
function ensureValidation(resp) {
|
||||
if (Math.floor(resp.statusCode / 100) != 2) {
|
||||
// Non-2XX response
|
||||
console.log("Authorization status request failed with code " + resp.statusCode)
|
||||
return;
|
||||
}
|
||||
|
||||
var body = "";
|
||||
resp.on('data', function(chunk) {
|
||||
body += chunk;
|
||||
});
|
||||
resp.on('end', function(chunk) {
|
||||
if (chunk) { body += chunk; }
|
||||
|
||||
var authz = JSON.parse(body);
|
||||
|
||||
if (authz.status == "pending") {
|
||||
setTimeout(function() {
|
||||
http.get(state.authorizationURL, ensureValidation);
|
||||
}, state.retryDelay);
|
||||
} else if (authz.status == "valid") {
|
||||
cli.spinner("Validating domain ... done", true);
|
||||
console.log();
|
||||
getCertificate();
|
||||
} else if (authz.status == "invalid") {
|
||||
console.log("The CA was unable to validate the file you provisioned.");
|
||||
return;
|
||||
} else {
|
||||
console.log("The CA returned an authorization in an unexpected state");
|
||||
console.log(JSON.stringify(authz, null, " "));
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getCertificate() {
|
||||
var csr = crypto.generateCSR(state.keyPair, state.domain);
|
||||
|
||||
var certificateMessage = JSON.stringify({
|
||||
csr: csr,
|
||||
authorizations: [ state.authorizationURL ]
|
||||
});
|
||||
var jws = crypto.generateSignature(state.keyPair, new Buffer(certificateMessage));
|
||||
var payload = JSON.stringify(jws);
|
||||
|
||||
cli.spinner("Requesting certificate");
|
||||
|
||||
var options = url.parse(state.newCertificateURL);
|
||||
options.method = "POST";
|
||||
var req = http.request(options, downloadCertificate);
|
||||
req.write(payload)
|
||||
req.end();
|
||||
}
|
||||
|
||||
function downloadCertificate(resp) {
|
||||
var chunks = [];
|
||||
resp.on('data', function(chunk) {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
resp.on('end', function(chunk) {
|
||||
if (chunk) { chunks.push(chunk); }
|
||||
var body = Buffer.concat(chunks);
|
||||
|
||||
if (Math.floor(resp.statusCode / 100) != 2) {
|
||||
// Non-2XX response
|
||||
console.log("Certificate request failed with code " + resp.statusCode);
|
||||
console.log(body.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
cli.spinner("Requesting certificate ... done", true);
|
||||
console.log();
|
||||
var certB64 = util.b64enc(body);
|
||||
|
||||
state.certificate = certB64;
|
||||
inquirer.prompt(questions.files, saveFiles);
|
||||
});
|
||||
}
|
||||
|
||||
function saveFiles(answers) {
|
||||
var keyPEM = crypto.privateKeyToPem(state.keyPair.privateKey);
|
||||
fs.writeFileSync(answers.keyFile, keyPEM);
|
||||
|
||||
var certPEM = crypto.certificateToPem(state.certificate);
|
||||
fs.writeFileSync(answers.certFile, certPEM);
|
||||
|
||||
console.log("Done!")
|
||||
console.log("To try it out:");
|
||||
console.log("openssl s_server -accept 8080 -www -key "+
|
||||
answers.keyFile +" -cert "+ answers.certFile);
|
||||
|
||||
// XXX: Explicitly exit, since something's tenacious here
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
|
||||
// BEGIN
|
||||
main();
|
||||
|
||||
Loading…
Reference in New Issue