Reformat code

This commit is contained in:
Pierre Lannoy 2023-12-12 19:54:00 +01:00
commit 5a6e557913
Signed by: Pierre Lannoy
GPG key ID: D27231EF87D53F31
4 changed files with 58 additions and 41 deletions

View file

@ -1,6 +1,6 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { promisify } from "util"; import {promisify} from "util";
import fromKeyLike from "jose/jwk/from_key_like"; import fromKeyLike from "jose/jwk/from_key_like";
import generateKeyPair from "jose/util/generate_key_pair"; import generateKeyPair from "jose/util/generate_key_pair";
import calculateThumbprint from "jose/jwk/thumbprint"; import calculateThumbprint from "jose/jwk/thumbprint";
@ -9,6 +9,7 @@ import CompactSign from "jose/jws/compact/sign";
import pem from "pem"; import pem from "pem";
import * as common from "./common.js"; import * as common from "./common.js";
import request from "./request.js"; import request from "./request.js";
const createCsr = promisify(pem.createCSR); const createCsr = promisify(pem.createCSR);
/** /**
@ -37,6 +38,7 @@ class Client {
this.replayNonce = ''; this.replayNonce = '';
this.thumbprint = ''; this.thumbprint = '';
} }
/** /**
* Export account public and private keys to a directory. * Export account public and private keys to a directory.
* *
@ -56,19 +58,21 @@ class Client {
common.writeKeyToFile(publicKeyFile, this.accountPublicKey) common.writeKeyToFile(publicKeyFile, this.accountPublicKey)
]); ]);
} }
/** /**
* Generate new account public and private keys. * Generate new account public and private keys.
* *
* @return {Promise} * @return {Promise}
*/ */
async generateAccountKeyPair() { async generateAccountKeyPair() {
const { privateKey, publicKey } = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM); const {privateKey, publicKey} = await generateKeyPair(common.ACCOUNT_KEY_ALGORITHM);
// @ts-ignore // @ts-ignore
this.accountPrivateKey = privateKey; this.accountPrivateKey = privateKey;
// @ts-ignore // @ts-ignore
this.accountPublicKey = publicKey; this.accountPublicKey = publicKey;
await this.initAccountJwks(); await this.initAccountJwks();
} }
/** /**
* Generate a certificate from Let's Encrypt for your domain. * Generate a certificate from Let's Encrypt for your domain.
* *
@ -81,16 +85,17 @@ class Client {
await this.newNonce(); await this.newNonce();
if (!this.myAccountUrl) if (!this.myAccountUrl)
await this.newAccount(); await this.newAccount();
const { authzUrls, finalizeUrl } = await this.newOrder(domain); const {authzUrls, finalizeUrl} = await this.newOrder(domain);
const { challenge } = await this.authz(authzUrls[0]); const {challenge} = await this.authz(authzUrls[0]);
await this.completeChallenge(challenge, domain); await this.completeChallenge(challenge, domain);
await this.pollAuthz(authzUrls[0]); await this.pollAuthz(authzUrls[0]);
const { certificate, privateKeyData } = await this.finalizeOrder(finalizeUrl, domain); const {certificate, privateKeyData} = await this.finalizeOrder(finalizeUrl, domain);
return { return {
certificate, certificate,
privateKeyData privateKeyData
}; };
} }
/** /**
* Import account public and private keys from a directory. * Import account public and private keys from a directory.
* *
@ -108,6 +113,7 @@ class Client {
this.accountPublicKey = common.importPublicKey(publicKeyData); this.accountPublicKey = common.importPublicKey(publicKeyData);
await this.initAccountJwks(); await this.initAccountJwks();
} }
async authz(authzUrl) { async authz(authzUrl) {
const data = await this.sign({ const data = await this.sign({
kid: this.myAccountUrl, kid: this.myAccountUrl,
@ -125,18 +131,20 @@ class Client {
if (res.statusCode !== 200) { if (res.statusCode !== 200) {
throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`); throw new Error(`authz() Status Code: ${res.statusCode} Data: ${res.data}`);
} }
const { challenges, identifier, ...rest } = res.data; const {challenges, identifier, ...rest} = res.data;
const challenge = challenges.find(({ type }) => type === 'http-01'); const challenge = challenges.find(({type}) => type === 'http-01');
return { return {
challenge, challenge,
domain: identifier.value, domain: identifier.value,
...rest ...rest
}; };
} }
async completeChallenge(challenge, domain) { async completeChallenge(challenge, domain) {
await this.readyChallenge(challenge); await this.readyChallenge(challenge);
await this.receiveServerRequest(challenge, domain); await this.receiveServerRequest(challenge, domain);
} }
async directory() { async directory() {
if (this.hasDirectory) if (this.hasDirectory)
return false; return false;
@ -150,6 +158,7 @@ class Client {
this.newOrderUrl = res.data.newOrder; this.newOrderUrl = res.data.newOrder;
return true; return true;
} }
async fetchCertificate(certificateUrl) { async fetchCertificate(certificateUrl) {
const data = await this.sign({ const data = await this.sign({
kid: this.myAccountUrl, kid: this.myAccountUrl,
@ -170,13 +179,15 @@ class Client {
} }
return res.data; return res.data;
} }
async finalizeOrder(finalizeUrl, domain) { async finalizeOrder(finalizeUrl, domain) {
const { privateKey } = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM); const {privateKey} = await generateKeyPair(common.CERTIFICATE_KEY_ALGORITHM);
// @ts-ignore // @ts-ignore
const clientKey = common.exportPrivateKey(privateKey); const clientKey = common.exportPrivateKey(privateKey);
let { csr let {
// @ts-ignore csr
} = await createCsr({ // @ts-ignore
} = await createCsr({
clientKey, clientKey,
commonName: domain, commonName: domain,
}); });
@ -215,6 +226,7 @@ class Client {
privateKeyData: clientKey privateKeyData: clientKey
}; };
} }
async initAccountJwks() { async initAccountJwks() {
if (this.accountPrivateKey == null || this.accountPublicKey == null) { if (this.accountPrivateKey == null || this.accountPublicKey == null) {
return Promise.reject(new Error('Account key pair not generated')); return Promise.reject(new Error('Account key pair not generated'));
@ -227,6 +239,7 @@ class Client {
this.accountPrivateJwk = accountPrivateJwk; this.accountPrivateJwk = accountPrivateJwk;
this.thumbprint = await calculateThumbprint(publicJwk); this.thumbprint = await calculateThumbprint(publicJwk);
} }
async newAccount(...emails) { async newAccount(...emails) {
const data = await this.sign({ const data = await this.sign({
jwk: this.accountPublicJwk, jwk: this.accountPublicJwk,
@ -249,6 +262,7 @@ class Client {
this.myAccountUrl = res.headers.location; this.myAccountUrl = res.headers.location;
return res.statusCode === 201; return res.statusCode === 201;
} }
async newNonce() { async newNonce() {
if (this.replayNonce) if (this.replayNonce)
return false; return false;
@ -261,6 +275,7 @@ class Client {
this.setReplayNonce(res); this.setReplayNonce(res);
return true; return true;
} }
async newOrder(...domains) { async newOrder(...domains) {
const identifiers = domains.map(domain => ({ const identifiers = domains.map(domain => ({
type: 'dns', type: 'dns',
@ -285,7 +300,7 @@ class Client {
throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`); throw new Error(`newOrder() Status Code: ${res.statusCode} Data: ${res.data}`);
} }
const orderUrl = res.headers.location; const orderUrl = res.headers.location;
const { authorizations: authzUrls, finalize: finalizeUrl } = res.data; const {authorizations: authzUrls, finalize: finalizeUrl} = res.data;
return { return {
authzUrls, authzUrls,
domains, domains,
@ -293,6 +308,7 @@ class Client {
orderUrl orderUrl
}; };
} }
async pollAuthz(authzUrl) { async pollAuthz(authzUrl) {
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const result = await this.authz(authzUrl); const result = await this.authz(authzUrl);
@ -307,6 +323,7 @@ class Client {
} }
throw new Error('pollAuthz() timed out'); throw new Error('pollAuthz() timed out');
} }
async readyChallenge(challenge) { async readyChallenge(challenge) {
const data = await this.sign({ const data = await this.sign({
kid: this.myAccountUrl, kid: this.myAccountUrl,
@ -325,6 +342,7 @@ class Client {
throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`); throw new Error(`readyChallenge() Status Code: ${res.statusCode} Data: ${res.data}`);
} }
} }
receiveServerRequest(challenge, domain) { receiveServerRequest(challenge, domain) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const time = setTimeout(() => { const time = setTimeout(() => {
@ -344,6 +362,7 @@ class Client {
}; };
}); });
} }
setReplayNonce(res) { setReplayNonce(res) {
const replayNonce = (res.headers['replay-nonce'] || '').trim(); const replayNonce = (res.headers['replay-nonce'] || '').trim();
if (!replayNonce) { if (!replayNonce) {
@ -351,6 +370,7 @@ class Client {
} }
this.replayNonce = replayNonce; this.replayNonce = replayNonce;
} }
/** /**
* @param {import("jose/types.js").JWSHeaderParameters} header * @param {import("jose/types.js").JWSHeaderParameters} header
* @param {import("jose/types.js").JWTPayload | undefined} [payload] * @param {import("jose/types.js").JWTPayload | undefined} [payload]
@ -363,12 +383,11 @@ class Client {
if (payload) { if (payload) {
data = await new SignJWT(payload) data = await new SignJWT(payload)
.setProtectedHeader({ .setProtectedHeader({
alg: common.ACCOUNT_KEY_ALGORITHM, alg: common.ACCOUNT_KEY_ALGORITHM,
...header ...header
}) })
.sign(this.accountPrivateKey); .sign(this.accountPrivateKey);
} } else {
else {
// SignJWT constructor only accepts object but RFC8555 requires empty payload // SignJWT constructor only accepts object but RFC8555 requires empty payload
// Workaround: manually pass empty Uint8Array to CompactSign constructor // Workaround: manually pass empty Uint8Array to CompactSign constructor
const sig = new CompactSign(new Uint8Array()); const sig = new CompactSign(new Uint8Array());
@ -386,4 +405,5 @@ class Client {
}); });
} }
} }
export default Client; export default Client;

View file

@ -1,5 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import fs from "fs"; import fs from "fs";
export const ACCOUNT_KEY_ALGORITHM = 'ES256'; export const ACCOUNT_KEY_ALGORITHM = 'ES256';
export const CERTIFICATE_KEY_ALGORITHM = 'RS256'; export const CERTIFICATE_KEY_ALGORITHM = 'RS256';
const env = (process.env.NODE_ENV || '').trim().toLowerCase(); const env = (process.env.NODE_ENV || '').trim().toLowerCase();
@ -61,8 +62,7 @@ export const importPrivateKey = (privateKeyData, passphrase) => {
} }
try { try {
return crypto.createPrivateKey(privateKeyOpts); return crypto.createPrivateKey(privateKeyOpts);
} } catch {
catch {
throw new Error('Failed to import private key'); throw new Error('Failed to import private key');
} }
}; };
@ -79,8 +79,7 @@ export const importPublicKey = publicKeyData => {
format: PUBLIC_KEY_FORMAT, format: PUBLIC_KEY_FORMAT,
type: PUBLIC_KEY_TYPE type: PUBLIC_KEY_TYPE
}); });
} } catch {
catch {
throw new Error('Failed to import public key'); throw new Error('Failed to import public key');
} }
}; };
@ -97,8 +96,7 @@ export const writeKeyToFile = async (filename, key, passphrase) => {
key = key.includes('PRIVATE KEY') key = key.includes('PRIVATE KEY')
? importPrivateKey(key, passphrase) ? importPrivateKey(key, passphrase)
: importPublicKey(key); : importPublicKey(key);
} } else if (!(key instanceof crypto.KeyObject)) {
else if (!(key instanceof crypto.KeyObject)) {
throw new Error('Expected "key" to be crypto.KeyObject or string'); throw new Error('Expected "key" to be crypto.KeyObject or string');
} }
const isPrivateKey = key.type === 'private'; const isPrivateKey = key.type === 'private';
@ -106,6 +104,6 @@ export const writeKeyToFile = async (filename, key, passphrase) => {
? exportPrivateKey(key, passphrase) ? exportPrivateKey(key, passphrase)
: exportPublicKey(key); : exportPublicKey(key);
const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS; const mode = isPrivateKey ? PRIVATE_KEY_PERMISSIONS : PUBLIC_KEY_PERMISSIONS;
await fs.promises.writeFile(filename, keyData, { mode }); await fs.promises.writeFile(filename, keyData, {mode});
}; };

View file

@ -1,3 +1,4 @@
import Client from './client.js'; import Client from './client.js';
export * from './common.js'; export * from './common.js';
export { Client }; export {Client};

View file

@ -6,33 +6,31 @@ import https from "https";
* @param {() => void} [cb] * @param {() => void} [cb]
* @return {Promise<{data: any, headers: import('http').IncomingHttpHeaders, statusCode: number}>} * @return {Promise<{data: any, headers: import('http').IncomingHttpHeaders, statusCode: number}>}
*/ */
const request = (url, { data = '', ...options } = {}, cb) => { const request = (url, {data = '', ...options} = {}, cb) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
url = new URL(url); url = new URL(url);
} } catch (err) {
catch (err) {
return reject(err); return reject(err);
} }
https.request(url, options, res => { https.request(url, options, res => {
const { statusCode, headers } = res; const {statusCode, headers} = res;
let data = ''; let data = '';
res res
.on('data', chunk => { .on('data', chunk => {
data += chunk; data += chunk;
}) })
.once('end', () => { .once('end', () => {
if (headers['content-type']?.includes('application/json')) { if (headers['content-type']?.includes('application/json')) {
try { try {
data = JSON.parse(data); data = JSON.parse(data);
} catch (err) {
reject(err);
return;
}
} }
catch (err) { resolve({data, headers, statusCode: statusCode || 0});
reject(err); })
return;
}
}
resolve({ data, headers, statusCode: statusCode || 0 });
})
.once('error', reject); .once('error', reject);
}) })
.once('error', reject) .once('error', reject)