Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,7 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ name: Release

on: workflow_dispatch

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -24,4 +32,5 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NPM_CONFIG_PROVENANCE: true
run: npx semantic-release --branches main
5 changes: 1 addition & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ certcli \
-p /path/to/account.pub \
-k /path/to/account.key \
-s /path/to/domain.csr \
-e test@example.com \
-c /path/to/domain.crt
```

Expand All @@ -66,15 +65,13 @@ Options:
-k, --priv_key Path to your account private key [string] [required]
-p, --pub_key Path to your account public key [string] [required]
-s, --csr Path to the certificate signing request [string] [required]
-e, --email Contact email [string] [required]
-c, --cert_file Path to save the certificate file [string]
-d, --dry_run Perform a dry run [count]
-h, --help Show help [boolean]

Examples:
certcli -k account.key -p account.pub -s Get a certificate and save to
domain.csr -e test@example.com -c `domain.crt`
domain.crt
domain.csr -c domain.crt `domain.crt`
https_proxy=http://localhost:8080 Using a HTTP proxy if the
certcli ... connectivity to Let's Encrypt API is
not stable
Expand Down
45 changes: 9 additions & 36 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
createOrder,
createOrderCmdAsync,
validateRegistrationAsync,
validateUpdateAsync,
validateOrderAsync,
buildAuthorizationAsync,
validateAuthorizationAsync,
Expand All @@ -45,12 +44,13 @@ import {
import {
opensslSignAsync,
} from "./openssl_exec";
import { sleep } from "./utils";

const argv = yargs(hideBin(process.argv))
.usage("Usage: $0 [options]")
.example([
[
"$0 -k account.key -p account.pub -s domain.csr -e test@example.com -c domain.crt",
"$0 -k account.key -p account.pub -s domain.csr -c domain.crt",
"Get a certificate and save to `domain.crt`"
],
[
Expand All @@ -76,12 +76,6 @@ const argv = yargs(hideBin(process.argv))
type: "string",
demandOption: true,
})
.option("email", {
alias: "e",
describe: "Contact email",
type: "string",
demandOption: true,
})
.option("cert_file", {
alias: "c",
describe: "Path to save the certificate file",
Expand Down Expand Up @@ -152,8 +146,6 @@ async function main() {
console.log(` - ${chalk.green(argv.priv_key)}`);
console.log(chalk.cyan("Public Key:"));
console.log(` - ${chalk.green(argv.pub_key)}`);
console.log(chalk.cyan("Email:"));
console.log(` - ${chalk.yellow(argv.email)}`);
console.log(chalk.cyan("Domains:"));
domains.forEach((domain) => {
console.log(` - ${chalk.magenta(domain)}`);
Expand Down Expand Up @@ -185,7 +177,6 @@ async function main() {

const stage_account = ora(`Validating account info...`).start();
const account = await createAccountAsync({
email: argv.email,
pubkey,
});
stage_account.succeed(`Account validated`);
Expand Down Expand Up @@ -213,45 +204,28 @@ async function main() {
const stage_term = ora("Accepting terms...").start();
const {
account_uri,
update_protected_b64,
cmd: registration_cmd,
order_protected_b64,
cmd: order_sig_cmd,
} = await validateRegistrationAsync({
directory,
account,
openssl_order_signature,
order,
registration_protected_b64,
});
const openssl_registration_signature = await opensslSignAsync({
content: registration_cmd,
priv_key: argv.priv_key,
});
stage_term.succeed("Terms accepted");

const stage_validate = ora(`Updating yout account email ${argv.email}...`).start();
const {
order_protected_b64,
cmd: order_sig_cmd,
} = await validateUpdateAsync({
account,
account_uri,
directory,
openssl_registration_signature,
order,
update_protected_b64,
});
const openssl_validate_order_signature = await opensslSignAsync({
content: order_sig_cmd,
priv_key: argv.priv_key,
});
stage_validate.succeed("Email updated");
stage_term.succeed("Terms accepted");

const stage_update_order = ora("Creating your certificate order").start();
const {
order_uri,
order_response,
} = await validateOrderAsync({
directory,
openssl_validate_order_signature,
openssl_registration_signature,
order,
order_protected_b64,
});
Expand Down Expand Up @@ -404,12 +378,10 @@ async function main() {

if (obj.status === "valid")
continue;

// FIXME
console.log(obj);
}

const stage_finalize_order = ora(`Finalizing order...`).start();
await sleep(10_000);
const {
finalize_payload_b64,
finalize_protected_b64,
Expand All @@ -425,6 +397,7 @@ async function main() {
content: finalize_order_cmd,
priv_key: argv.priv_key,
});
await sleep(1000);
const {
recheck_order_protected_b64,
cmd: foo,
Expand Down
77 changes: 7 additions & 70 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,13 @@ interface Account {
termsOfServiceAgreed: boolean;
};
registration_payload_b64: string;
update_payload_json: {
contact: string[];
};
update_payload_b64: string;
}

export async function createAccountAsync({
email,
pubkey,
}: {
email: string;
pubkey: string;
}): Promise<Account> {
// validate email
const email_re = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
if (!email_re.test(email)) {
throw "Account email doesn't look valid.";
}

// parse account public key
if (pubkey === "") {
throw "You need to include an account public key.";
Expand All @@ -147,9 +135,6 @@ export async function createAccountAsync({
let registration_payload_json = {
termsOfServiceAgreed: true,
};
let update_payload_json = {
contact: [ "mailto:" + email ],
};

return {
pubkey,
Expand All @@ -158,8 +143,6 @@ export async function createAccountAsync({
thumbprint: b64(hash),
registration_payload_json,
registration_payload_b64: b64(JSON.stringify(registration_payload_json)),
update_payload_json,
update_payload_b64: b64(JSON.stringify(update_payload_json)),
};
}

Expand Down Expand Up @@ -213,11 +196,13 @@ export async function validateRegistrationAsync({
account,
directory,
openssl_order_signature,
order,
registration_protected_b64,
}: {
account: Account,
directory: DirectoryResponse,
openssl_order_signature: string;
order: ReturnType<typeof createOrder>;
registration_protected_b64: string,
}) {
const signature = hex2b64(openssl_order_signature);
Expand Down Expand Up @@ -247,54 +232,6 @@ export async function validateRegistrationAsync({

const nonce = await getNonceAsync(directory);

const update_protected_json = {
url: account_uri,
alg: account.alg,
nonce,
kid: account_uri,
};
const update_protected_b64 = b64(JSON.stringify(update_protected_json));

return {
account_uri,
update_protected_b64,
cmd: `${update_protected_b64}.${account.update_payload_b64}`
};
}

// Step: 3b
export async function validateUpdateAsync({
account,
account_uri,
directory,
openssl_registration_signature,
order,
update_protected_b64,
}: {
account: Account;
account_uri: string;
directory: DirectoryResponse;
openssl_registration_signature: string;
order: ReturnType<typeof createOrder>;
update_protected_b64: string;
}) {
const signature = hex2b64(openssl_registration_signature);
const response = await fetch(account_uri, {
method: "POST",
headers: FETCH_HEADERS,
body: JSON.stringify({
protected: update_protected_b64,
payload: account.update_payload_b64,
signature,
}),
});

if (response.status != 200) {
throw "Account contact update failed. Please start back at Step 1.";
}

const nonce = await getNonceAsync(directory);

const order_protected_json = {
url: directory.newOrder,
alg: account.alg,
Expand All @@ -305,26 +242,27 @@ export async function validateUpdateAsync({
const order_protected_b64 = b64(JSON.stringify(order_protected_json));

return {
account_uri,
order_protected_b64,
cmd: `${order_protected_b64}.${order.order_payload_b64}`,
};
}

/*
* Step 3c: Create New Order (POST /newOrder)
* Step 3b: Create New Order (POST /newOrder)
*/
export async function validateOrderAsync({
directory,
openssl_validate_order_signature,
openssl_registration_signature,
order,
order_protected_b64,
}: {
directory: DirectoryResponse;
openssl_validate_order_signature: string;
openssl_registration_signature: string;
order: ReturnType<typeof createOrder>;
order_protected_b64: string;
}) {
const signature = hex2b64(openssl_validate_order_signature);
const signature = hex2b64(openssl_registration_signature);
const response = await fetch(directory.newOrder, {
method: "POST",
headers: FETCH_HEADERS,
Expand Down Expand Up @@ -535,7 +473,6 @@ export async function checkAuthorizationAsync({
const status = auth_obj.status;

if (status === "pending") {
console.log(auth_obj);
const nonce = await getNonceAsync(directory);
const recheck_auth_protected_json = {
url: challenge.url,
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { setTimeout } from "node:timers/promises";

export async function sleep(delay: number) {
await setTimeout(delay);
}
Loading