Merge remote-tracking branch 'upstream/master' into revoker

This commit is contained in:
Roland Shoemaker 2015-05-25 01:25:42 +01:00
commit 461f03bb11
8 changed files with 193 additions and 66 deletions

View File

@ -63,7 +63,6 @@ else
run go test ${dirlist}
fi
echo "Checking for unformatted files:"
unformatted=$(find . -name "*.go" -not -path "./Godeps/*" -print | xargs -n1 gofmt -l)
if [ "x${unformatted}" != "x" ] ; then
echo "Unformatted files found; setting failure state."

View File

@ -0,0 +1,64 @@
{
"syslog": {
"network": "",
"server": "",
"tag": "boulder"
},
"amqp": {
"server": "amqp://guest:guest@localhost:5672",
"RA": {
"client": "RA.client",
"server": "RA.server"
},
"VA": {
"client": "VA.client",
"server": "VA.server"
},
"SA": {
"client": "SA.client",
"server": "SA.server"
},
"CA": {
"client": "CA.client",
"server": "CA.server"
}
},
"statsd": {
"server": "localhost:8125",
"prefix": "Boulder"
},
"wfe": {
"baseURL": "http://localhost:4300",
"listenAddress": "0.0.0.0:4300"
},
"ca": {
"server": "localhost:9300",
"authKey": "79999d86250c367a2b517a1ae7d409c1",
"serialPrefix": 255,
"profile": "ee",
"dbDriver": "sqlite3",
"dbName": ":memory:",
"testMode": true,
"issuerCert": "test/test-ca.pem",
"_comment": "This should only be present in testMode. In prod use an HSM.",
"issuerKey": "test/test-ca.key"
},
"sa": {
"dbDriver": "sqlite3",
"dbName": ":memory:"
},
"mail": {
"server": "mail.example.com",
"port": "25",
"username": "cert-master@example.com",
"password": "password"
},
"subscriberAgreementURL": "http://localhost:4300/terms"
}

38
test/integration-test.sh Executable file
View File

@ -0,0 +1,38 @@
#!/bin/bash
cd $(dirname $0)/..
# Ensure cleanup
trap "trap '' SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
go run ./cmd/boulder/main.go --config test/boulder-test-config.json &>/dev/null &
go run Godeps/_workspace/src/github.com/cloudflare/cfssl/cmd/cfssl/cfssl.go \
-loglevel 0 \
serve \
-port 9300 \
-ca test/test-ca.pem \
-ca-key test/test-ca.key \
-config test/cfssl-config.json &>/dev/null &
cd test/js
npm install
# Wait for Boulder to come up
until nc localhost 4300 < /dev/null ; do sleep 1 ; done
CERT_KEY=$(mktemp /tmp/cert_XXXXX.pem)
CERT=$(mktemp /tmp/cert_XXXXX.crt)
node test.js --email foo@bar.com --agree true \
--domain foo.com --new-reg http://localhost:4300/acme/new-reg \
--certKey ${CERT_KEY} --cert ${CERT} && \
node revoke.js ${CERT} ${CERT_KEY} http://localhost:4300/acme/revoke-cert/
STATUS=$?
# Cleanup
rm -f ${CERT_KEY}
rm -f ${CERT}
rm -f account-key.pem
rm -f temp-cert.pem
exit $STATUS

View File

@ -24,7 +24,4 @@ The node.js scripts in this directory provide a simple end-to-end test of Boulde
# Client side
mkdir -p .well-known/acme-challenge/
node test.js
mv -- *.txt .well-known/acme-challenge/ # In a different window
python -m SimpleHTTPServer 5001 # In yet another window

View File

@ -5,6 +5,7 @@
"version": "0.0.1",
"dependencies": {
"cli": "^0.6.5",
"colors": "^1.1.0",
"inquirer": "^0.8.2",
"node-forge": "^0.6.21",
"request": "^2.55.0"

View File

@ -15,12 +15,13 @@ var fs = require('fs');
var request = require('request');
function main() {
if (process.argv.length != 4) {
console.log('Usage: js revoke.js cert.der key.pem');
if (process.argv.length != 5) {
console.log('Usage: js revoke.js cert.der key.pem REVOKE_URL');
process.exit(1);
}
var key = crypto.importPemPrivateKey(fs.readFileSync(process.argv[3]));
var certDER = fs.readFileSync(process.argv[2])
var revokeUrl = process.argv[4];
var certDERB64URL = util.b64enc(new Buffer(certDER))
var revokeMessage = JSON.stringify({
// For some reason forge adds an extra, incorrect '00' at the front of the
@ -29,7 +30,7 @@ function main() {
});
console.log('Requesting revocation:', revokeMessage)
var jws = crypto.generateSignature(key, new Buffer(revokeMessage));
var req = request.post('http://localhost:4000/acme/revoke-cert/', function(err, resp) {
var req = request.post(revokeUrl, function(err, resp) {
if (err) {
console.log('Error: ', err);
}

View File

@ -14,6 +14,7 @@
"use strict";
var colors = require("colors");
var cli = require("cli");
var crypto = require("./crypto-util");
var child_process = require('child_process');
@ -80,19 +81,30 @@ var questions = {
}],
};
var cliOptions = cli.parse({
// To test against the demo instance, pass --newReg "https://www.letsencrypt-demo.org/acme/new-reg"
// To get a cert from the demo instance, you must be publicly reachable on
// port 443 under the DNS name you are trying to get, and run test.js as root.
newReg: ["new-reg", "New Registration URL", "string", "http://localhost:4000/acme/new-reg"],
certKeyFile: ["certKey", "File for cert key (created if not exists)", "path", "cert-key.pem"],
certFile: ["cert", "Path to output certificate (DER format)", "path", "cert.pem"],
email: ["email", "Email address", "string", null],
agreeTerms: ["agree", "Agree to terms of service", "boolean", null],
domain: ["domain", "Domain name for which to request a certificate", "string", null],
});
var state = {
certPrivateKey: null,
accountPrivateKey: null,
//newRegistrationURL: "https://www.letsencrypt-demo.org/acme/new-reg",
newRegistrationURL: "http://localhost:4000/acme/new-reg",
newRegistrationURL: cliOptions.newReg,
registrationURL: "",
termsRequired: false,
termsAgreed: false,
termsAgreed: null,
termsURL: null,
domain: null,
domain: cliOptions.domain,
newAuthorizationURL: "",
authorizationURL: "",
@ -102,8 +114,8 @@ var state = {
newCertificateURL: "",
certificateURL: "",
certFile: "",
keyFile: ""
certFile: cliOptions.certFile,
keyFile: cliOptions.certKeyFile,
};
function parseLink(link) {
@ -136,11 +148,32 @@ function post(url, body, callback) {
var jws = crypto.generateSignature(state.accountPrivateKey, new Buffer(payload));
var signed = JSON.stringify(jws, null, 2);
var req = request.post(url, callback);
console.log('Posting to', url, ':\n', signed);
console.log('Payload:', payload);
console.log('Posting to', url, ':');
console.log(signed.green);
console.log('Payload:')
console.log(payload.blue);
var req = request.post({
url: url,
encoding: null // Return body as buffer, needed for certificate response
}, function(error, response, body) {
// Don't print non-ASCII characters (like DER-encoded cert) to the terminal
if (body && !body.toString().match(/[^\x00-\x7F]/)) {
try {
var parsed = JSON.parse(body);
console.log(JSON.stringify(parsed, null, 2).cyan);
} catch (e) {
console.log(body.toString().cyan);
}
}
callback(error, response, body)
});
req.on('response', function(response) {
console.log(response.headers)
Object.keys(response.headers).forEach(function(key) {
var value = response.headers[key];
var upcased = key.charAt(0).toUpperCase() + key.slice(1);
console.log((upcased + ": " + value).yellow)
});
console.log()
})
req.write(signed)
req.end();
@ -183,12 +216,10 @@ saveFiles
*/
function main() {
inquirer.prompt(questions.files, makeKeyPair);
makeKeyPair();
}
function makeKeyPair(answers) {
state.certFile = answers.certFile;
state.keyFile = answers.keyFile;
function makeKeyPair() {
console.log("Generating cert key pair...");
child_process.exec("openssl req -newkey rsa:2048 -keyout " + state.keyFile + " -days 3650 -subj /CN=foo -nodes -x509 -out temp-cert.pem", function (error, stdout, stderr) {
if (error) {
@ -198,7 +229,7 @@ function makeKeyPair(answers) {
state.certPrivateKey = crypto.importPemPrivateKey(fs.readFileSync(state.keyFile));
console.log();
makeAccountKeyPair(answers)
makeAccountKeyPair()
});
}
@ -212,7 +243,11 @@ function makeAccountKeyPair(answers) {
state.accountPrivateKey = crypto.importPemPrivateKey(fs.readFileSync("account-key.pem"));
console.log();
inquirer.prompt(questions.email, register)
if (cliOptions.email) {
register({email: cliOptions.email});
} else {
inquirer.prompt(questions.email, register)
}
});
}
@ -229,13 +264,13 @@ function getTerms(err, resp) {
if (err || Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Registration request failed:" + err);
return;
process.exit(1);
}
var links = parseLink(resp.headers["link"]);
if (!links || !("next" in links)) {
console.log("The server did not provide information to proceed");
return
process.exit(1);
}
state.registrationURL = resp.headers["location"];
@ -262,7 +297,11 @@ function getAgreement(err, resp, body) {
console.log(body);
console.log();
inquirer.prompt(questions.terms, sendAgreement);
if (!cliOptions.agreeTerms) {
inquirer.prompt(questions.terms, sendAgreement);
} else {
sendAgreement({terms: true});
}
}
function sendAgreement(answers) {
@ -286,7 +325,11 @@ function sendAgreement(answers) {
console.log("Couldn't POST agreement back to server, aborting.");
process.exit(1);
} else {
inquirer.prompt(questions.domain, getChallenges);
if (!state.domain) {
inquirer.prompt(questions.domain, getChallenges);
} else {
getChallenges({domain: state.domain});
}
}
});
}
@ -307,13 +350,13 @@ function getReadyToValidate(err, resp, body) {
if (err || Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Authorization request failed with code " + resp.statusCode)
return;
process.exit(1);
}
var links = parseLink(resp.headers["link"]);
if (!links || !("next" in links)) {
console.log("The server did not provide information to proceed");
return
process.exit(1);
}
state.authorizationURL = resp.headers["location"];
@ -324,19 +367,18 @@ function getReadyToValidate(err, resp, 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;
process.exit(1);
}
var challenge = simpleHttps[0];
var path = crypto.randomString(8) + ".txt";
var challengePath = ".well-known/acme-challenge/" + path;
fs.writeFileSync(challengePath, challenge.token);
state.responseURL = challenge["uri"];
state.path = path;
// For local, test-mode validation
function httpResponder(req, response) {
console.log("Got request for", req.url);
console.log("\nGot request for", req.url);
var host = req.headers["host"];
if ((host === state.domain || /localhost/.test(state.newRegistrationURL)) &&
req.method === "GET" &&
@ -348,7 +390,7 @@ function getReadyToValidate(err, resp, body) {
response.writeHead(404, {"Content-Type": "text/plain"});
response.end("");
}
};
}
if (/localhost/.test(state.newRegistrationURL)) {
var httpServer = http.createServer(httpResponder)
httpServer.listen(5001)
@ -360,10 +402,6 @@ function getReadyToValidate(err, resp, body) {
httpServer.listen(443)
}
inquirer.prompt(questions.readyToValidate, sendResponse);
}
function sendResponse() {
cli.spinner("Validating domain");
post(state.responseURL, {
path: state.path
@ -374,7 +412,7 @@ function ensureValidation(err, resp, body) {
if (Math.floor(resp.statusCode / 100) != 2) {
// Non-2XX response
console.log("Authorization status request failed with code " + resp.statusCode)
return;
process.exit(1);
}
var authz = JSON.parse(body);
@ -389,35 +427,21 @@ function ensureValidation(err, resp, body) {
getCertificate();
} else if (authz.status == "invalid") {
console.log("The CA was unable to validate the file you provisioned:" + body);
return;
process.exit(1);
} else {
console.log("The CA returned an authorization in an unexpected state");
console.log(JSON.stringify(authz, null, " "));
return;
process.exit(1);
}
}
function getCertificate() {
cli.spinner("Requesting certificate");
var csr = crypto.generateCSR(state.certPrivateKey, state.domain);
var certificateMessage = JSON.stringify({
post(state.newCertificateURL, {
csr: csr,
authorizations: [ state.authorizationURL ]
});
var jws = crypto.generateSignature(state.accountPrivateKey, new Buffer(certificateMessage));
var payload = JSON.stringify(jws);
cli.spinner("Requesting certificate");
var url = state.newCertificateURL;
console.log('Posting to', url, ':\n', payload);
var req = request.post({
url: url,
encoding: null // Return body as buffer.
}, downloadCertificate);
req.write(payload)
req.end();
}
function downloadCertificate(err, resp, body) {
@ -425,7 +449,7 @@ function downloadCertificate(err, resp, body) {
// Non-2XX response
console.log("Certificate request failed with code " + resp.statusCode);
console.log(body.toString());
return;
process.exit(1);
}
cli.spinner("Requesting certificate ... done", true);

View File

@ -275,7 +275,10 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
return
}
regURL := fmt.Sprintf("%s%s", wfe.RegBase, string(reg.ID))
// Use an explicitly typed variable. Otherwise `go vet' incorrectly complains
// that reg.ID is a string being passed to %d.
var id int64 = reg.ID
regURL := fmt.Sprintf("%s%d", wfe.RegBase, id)
responseBody, err := json.Marshal(reg)
if err != nil {
wfe.sendError(response, "Error marshaling authz", err, http.StatusInternalServerError)
@ -285,8 +288,8 @@ func (wfe *WebFrontEndImpl) NewRegistration(response http.ResponseWriter, reques
response.Header().Add("Location", regURL)
response.Header().Set("Content-Type", "application/json")
response.Header().Add("Link", link(wfe.NewAuthz, "next"))
if len(wfe.TermsPath) > 0 {
response.Header().Add("Link", link(wfe.BaseURL+wfe.TermsPath, "terms-of-service"))
if len(wfe.SubscriberAgreementURL) > 0 {
response.Header().Add("Link", link(wfe.SubscriberAgreementURL, "terms-of-service"))
}
response.WriteHeader(http.StatusCreated)
@ -357,15 +360,13 @@ func (wfe *WebFrontEndImpl) RevokeCertificate(response http.ResponseWriter, requ
return
}
body, requestKey, reg, err := wfe.verifyPOST(request, false)
// We don't ask verifyPOST to verify there is a correponding registration,
// because anyone with the right private key can revoke a certificate.
body, requestKey, _, err := wfe.verifyPOST(request, false)
if err != nil {
wfe.sendError(response, "Unable to read/verify body", err, http.StatusBadRequest)
return
}
if reg.Agreement == "" {
wfe.sendError(response, "Must agree to subscriber agreement before any further actions", nil, http.StatusForbidden)
return
}
type RevokeRequest struct {
CertificateDER core.JsonBuffer `json:"certificate"`
@ -618,7 +619,9 @@ func (wfe *WebFrontEndImpl) Registration(response http.ResponseWriter, request *
}
if len(update.Agreement) > 0 && update.Agreement != wfe.SubscriberAgreementURL {
wfe.sendError(response, fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]", update.Agreement, wfe.SubscriberAgreementURL), nil, http.StatusBadRequest)
wfe.sendError(response,
fmt.Sprintf("Provided agreement URL [%s] does not match current agreement URL [%s]",
update.Agreement, wfe.SubscriberAgreementURL), nil, http.StatusBadRequest)
return
}