Skip to content

Commit

Permalink
acme directory url now optional (Let's Encrypt is default); wait 15 s…
Browse files Browse the repository at this point in the history
…econds for acme server to catch up with added cloudflare dns record; readme: cloudflare cli example without sudo; added note for http challenges without sudo; etc.
  • Loading branch information
wille-io committed May 23, 2024
1 parent 9c09739 commit 650d6af
Show file tree
Hide file tree
Showing 5 changed files with 45 additions and 26 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Mike Wille
Copyright (c) 2024 Mike Wille

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,29 @@ Use the CLI as a standalone acme client, or use the acme.ts library to use it in

## CLI
How to get & use the CLI:
```
sudo deno install -A --allow-read=. --allow-write=. --allow-net --name acme --root /usr/local/ https://deno.land/x/acme@v0.3.1/cli.ts
```bash
sudo deno install -A --allow-read=. --allow-write=. --allow-net --name acme --root /usr/local/ https://deno.land/x/acme@v0.4.0/cli.ts
# http challenge:
sudo acme http example.com,subdomain.example.com
# cloudflare dns challenge:
sudo acme cloudflare example.com,subdomain.example.com
acme cloudflare example.com,subdomain.example.com
```
Note: For http challenges permissions to bind to port 80 are needed. Otherwise use the root user or use `sudo` - like in the example above.

## Library
To use acme as a library in your application, add the following:
```
import * as ACME from "https://deno.land/x/acme@v0.3.1/acme.ts"
To use acme as a library in your application, add the following (minimal example with temporary & anonymous acme account creation):
```typescript
import * as ACME from "https://deno.land/x/acme@v0.4.0/acme.ts"

// http challenge:
const { domainCertificates } = await ACME.getCertificatesWithHttp("example.com", "https://acme-staging-v02.api.letsencrypt.org/directory");
const { domainCertificates } = await ACME.getCertificatesWithHttp("example.com");
console.log(domainCertificates);

// cloudflare dns challenge:
const cloudflareToken = Deno.env.get("CLOUDFLARE_TOKEN");
const { domainCertificates } = await ACME.getCertificatesWithCloudflare(cloudflareToken, "example.com", "https://acme-staging-v02.api.letsencrypt.org/directory");
const { domainCertificates } = await ACME.getCertificatesWithCloudflare(cloudflareToken, "example.com");
console.log(domainCertificates);
```

## License
MIT
MIT
33 changes: 22 additions & 11 deletions acme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,41 +34,52 @@ async function createSession(acmeDirectoryUrl: string, options?: { pemAccountKey
}


export async function getCertificateWithHttp(domainName: string, acmeDirectoryUrl: string, options?: { yourEmail?: string,
function getAcmeDirectoryUrl(acmeDirectoryUrl?: string): string
{
if (acmeDirectoryUrl)
return acmeDirectoryUrl;
console.warn("IMPORTANT: By not supplying a acme directory url, you are always accepting Let's Encrypt's current general Terms of Service and their Subscriber Agreement which you can find at 'https://acme-v02.api.letsencrypt.org/directory' in json key 'meta.termsOfService'");
return "https://acme-v02.api.letsencrypt.org/directory";
}


export async function getCertificateWithHttp(domainName: string, options?: { acmeDirectoryUrl: string, yourEmail?: string,
pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo })
: Promise<{domainCertificate: DomainCertificate, pemAccountKeys: AccountKeys}>
{
const ret = await getCertificatesWithHttp([{ domainName }], acmeDirectoryUrl, options);
const ret = await getCertificatesWithHttp([{ domainName }], options);
return { domainCertificate: ret.domainCertificates[0], pemAccountKeys: ret.pemAccountKeys };
}


export async function getCertificatesWithHttp(domains: Domain[], acmeDirectoryUrl: string, options?: { yourEmail?: string,
export async function getCertificatesWithHttp(domains: Domain[], options?: { acmeDirectoryUrl: string, yourEmail?: string,
pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo })
: Promise<{domainCertificates: DomainCertificate[], pemAccountKeys: AccountKeys}>
{
const session = await createSession(acmeDirectoryUrl, { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail });
const session = await createSession(getAcmeDirectoryUrl(options?.acmeDirectoryUrl),
{ pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail });
return {
domainCertificates: await new ACMEHttp(session, domains, options?.yourEmail, options?.csrInfo).getCertificates(),
pemAccountKeys: await session.exportAccount(),
};
}


export async function getCertificateWithCloudflare(bearer: string, domainName: string, acmeDirectoryUrl: string, options: { yourEmail?: string,
export async function getCertificateWithCloudflare(bearer: string, domainName: string, options?: { acmeDirectoryUrl: string, yourEmail?: string,
pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo })
: Promise<{domainCertificate: DomainCertificate, pemAccountKeys: AccountKeys}>
{
const ret = await getCertificatesWithCloudflare(bearer, [{ domainName }], acmeDirectoryUrl, options);
const ret = await getCertificatesWithCloudflare(bearer, [{ domainName }], options);
return { domainCertificate: ret.domainCertificates[0], pemAccountKeys: ret.pemAccountKeys };
}


export async function getCertificatesWithCloudflare(bearer: string, domains: Domain[], acmeDirectoryUrl: string, options?: { yourEmail?: string,
export async function getCertificatesWithCloudflare(bearer: string, domains: Domain[], options?: { acmeDirectoryUrl: string, yourEmail?: string,
pemAccountKeys?: AccountKeys, csrInfo?: CSRInfo })
: Promise<{domainCertificates: DomainCertificate[], pemAccountKeys: AccountKeys}>
{
const session = await createSession(acmeDirectoryUrl, { pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail });
const session = await createSession(getAcmeDirectoryUrl(options?.acmeDirectoryUrl),
{ pemAccountKeys: options?.pemAccountKeys, email: options?.yourEmail });
return {
domainCertificates: await new ACMECloudflare(bearer, session, domains, options?.yourEmail, options?.csrInfo).getCertificates(),
pemAccountKeys: await session.exportAccount(),
Expand Down Expand Up @@ -817,8 +828,8 @@ class ACMECloudflare extends ACMEBase


// creating txt record(s) done - waiting for acme's dns to catch up
console.log("giving the acme server time (3s) to catch up...");
await waitSeconds(3);
console.log("giving the acme server time (15s) to catch up...");
await waitSeconds(15); // TODO: shorter wait, try again if 'invalid' for n times until not 'invalid' anymore


// fire all challenges
Expand Down Expand Up @@ -951,7 +962,7 @@ class ACMEHttp extends ACMEBase


// TODO: don't use serve
serve((request: Request): Response =>
serve((request: Request): Response => // TODO: AbortController
{
//console.log("!!!");
if (!request.url.endsWith("/.well-known/acme-challenge/" + token))
Expand Down
12 changes: 7 additions & 5 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ async function shared2(accountDirectory: string, accountKeys: ACME.AccountKeys |

const command = new Command()
.name("acme-cli")
.version("v0.3.1")
.version("v0.4.0")
.description("Get certificates for your domains and or your domains their subdomains with the specified challenge type from an acme server. \r\n"+
"One certificate is created per challenge argument. \r\n"+
"You can either get a certificate for a domain *and* its subdomains or for a domain only (without subdomains). It is not possible to get a certificate with only subdomains (without its parent domain).\r\n"+
"Subdomains are added to the domain name with commas. Example: example.com,subdomain.example.com,another-subdomain.example.com")
"You can either get a certificate for a domain *and* its subdomains or for a domain only (without subdomains). It is not possible to get a certificate with only subdomains (without its parent domain). \r\n"+
"Subdomains are added to the domain name with commas. Example: example.com,subdomain.example.com,another-subdomain.example.com \r\n" +
"IMPORTANT: By not supplying a acme directory url, you are always accepting Let's Encrypt's current general Terms of Service and their Subscriber Agreement which you can find at 'https://acme-v02.api.letsencrypt.org/directory' in json key 'meta.termsOfService'"
)
.globalOption("-d, --directory <directory>", "Https url to the acme server's acme directory.", { default: "https://acme-v02.api.letsencrypt.org/directory" as const })
.globalOption("-e, --email <email>", "Your email address for the acme server to notify you on notifications of your certificates")
.globalOption("-a, --accountDirectory <accountDirectory>", "The directory where the account keys of your acme server will be read from / written to", { default: `${Deno.env.get("HOME") || Deno.cwd()}/.deno-acme` as const })
Expand All @@ -143,7 +145,7 @@ const command = new Command()
const { domains, accountKeys } = await shared1(accountDirectory, domainsWithSubdomains);

const { domainCertificates, pemAccountKeys } =
await ACME.getCertificatesWithHttp(domains, directory, { yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ });
await ACME.getCertificatesWithHttp(domains, { acmeDirectoryUrl: directory, yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ });

await shared2(accountDirectory, accountKeys, pemAccountKeys, domainCertificates);
Deno.exit(0);
Expand All @@ -164,7 +166,7 @@ const command = new Command()
const { domains, accountKeys } = await shared1(accountDirectory, domainsWithSubdomains);

const { domainCertificates, pemAccountKeys } =
await ACME.getCertificatesWithCloudflare(cloudflareBearer, domains, directory, { yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ });
await ACME.getCertificatesWithCloudflare(cloudflareBearer, domains, { acmeDirectoryUrl: directory, yourEmail: email, pemAccountKeys: accountKeys, /*csrInfo*/ });

await shared2(accountDirectory, accountKeys, pemAccountKeys, domainCertificates);
Deno.exit(0);
Expand Down
5 changes: 5 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@mw/acme",
"version": "0.4.0",
"exports": "./acme.ts"
}

0 comments on commit 650d6af

Please sign in to comment.