Sure. Here's the key rotation script I was testing with. It's in Node.js, with the only dependency being first running npm install acme
to be able to use ACME.js and related libraries.
There's probably a better way to do it, but I've had fun learning how the protocol actually works. It reads the account id and key from files, and then writes the new key to a new file.
[I also have a version of this in AWS Lambda that reads and writes from the AWS Systems Manager Parameter Store instead of files (storing the key in the same place my hobby AWS Lambda cert renewal pulls it from), so I could just schedule a key rotation in AWS for any interval if I knew what rotation interval I "should" use. Hence my initial question here.]
I dedicate this code, such as it is, to the public domain, on the off chance that someone else finds it useful.
"use strict";
const ACME = require('acme');
const Keypairs = require('@root/keypairs');
const fs = require("fs").promises;
const maintainerEmail = "pete-acme-lambda-renewal@cooperjr.name";
const directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
const packageAgent = "acme-lambda-renewal/1.0.0";
const accountId = require("./acct-key-id.json");
const oldAccountPrivKey = require("./acct-key-priv-1.json");
const newAccountKeyFilename = "acct-key-priv-2.json";
function notify(ev, msg) {
console.log("acme notify", ev, msg.altname || '', msg.status || '', msg.message || '');
}
async function getNewNonce(acme) {
const newNonceUrl = acme._directoryUrls.newNonce;
const response = await acme.request({
method: 'HEAD',
url: newNonceUrl
});
return response.headers['replay-nonce'];
}
async function rotateKey() {
const acme = ACME.create({ maintainerEmail, packageAgent, notify });
await acme.init(directoryUrl);
const keyChangeUrl = acme._directoryUrls.keyChange;
//console.log("keyChangeUrl", keyChangeUrl);
const noncePromise = getNewNonce(acme);
const newAccountKeypair = await Keypairs.generate({ kty: 'EC', format: 'jwk' });
const newAccountPrivKey = newAccountKeypair.private;
const newAccountPubKey = newAccountKeypair.public;
const oldAccountPubKey = Keypairs.neuter({jwk: oldAccountPrivKey});
const innerRequest = await Keypairs.signJws({
jwk: newAccountPrivKey,
protected: {
url: keyChangeUrl,
jwk: newAccountPubKey,
kid: false
},
payload: {
account: accountId,
oldKey: oldAccountPubKey
}
});
const outerRequest = await Keypairs.signJws({
jwk: oldAccountPrivKey,
protected: {
"kid": accountId,
"nonce": await noncePromise,
"url": keyChangeUrl
},
payload: innerRequest
});
try {
const keyChangeResponse = await acme.request({url: keyChangeUrl, json: outerRequest});
const statusCode = keyChangeResponse.statusCode;
console.log("statusCode", statusCode);
console.log("headers", JSON.stringify(keyChangeResponse.headers));
console.log("body", keyChangeResponse.body);
if (statusCode == 200) {
await fs.writeFile(newAccountKeyFilename, JSON.stringify(newAccountPrivKey));
}
} catch (err) {
console.log("error", err);
throw err;
}
}
rotateKey().then(console.log).catch(console.log);