@distilled.cloud/acme 0.0.0-placeholder → 1.0.0-rc.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +73 -0
- package/lib/credentials.d.ts +50 -0
- package/lib/credentials.d.ts.map +1 -0
- package/lib/credentials.js +57 -0
- package/lib/credentials.js.map +1 -0
- package/lib/errors.d.ts +73 -0
- package/lib/errors.d.ts.map +1 -0
- package/lib/errors.js +50 -0
- package/lib/errors.js.map +1 -0
- package/lib/index.d.ts +30 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +30 -0
- package/lib/index.js.map +1 -0
- package/lib/jose.d.ts +66 -0
- package/lib/jose.d.ts.map +1 -0
- package/lib/jose.js +128 -0
- package/lib/jose.js.map +1 -0
- package/lib/protocol.d.ts +27 -0
- package/lib/protocol.d.ts.map +1 -0
- package/lib/protocol.js +410 -0
- package/lib/protocol.js.map +1 -0
- package/lib/response-validation.test.d.ts +4 -0
- package/lib/response-validation.test.d.ts.map +1 -0
- package/lib/response-validation.test.js +51 -0
- package/lib/response-validation.test.js.map +1 -0
- package/lib/retry.d.ts +33 -0
- package/lib/retry.d.ts.map +1 -0
- package/lib/retry.js +33 -0
- package/lib/retry.js.map +1 -0
- package/lib/services/acme.d.ts +446 -0
- package/lib/services/acme.d.ts.map +1 -0
- package/lib/services/acme.js +597 -0
- package/lib/services/acme.js.map +1 -0
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.d.ts.map +1 -0
- package/lib/services/index.js +3 -0
- package/lib/services/index.js.map +1 -0
- package/lib/traits.d.ts +13 -0
- package/lib/traits.d.ts.map +1 -0
- package/lib/traits.js +13 -0
- package/lib/traits.js.map +1 -0
- package/package.json +75 -7
- package/src/credentials.ts +106 -0
- package/src/errors.ts +101 -0
- package/src/index.ts +33 -0
- package/src/jose.ts +234 -0
- package/src/protocol.ts +582 -0
- package/src/response-validation.test.ts +65 -0
- package/src/retry.ts +55 -0
- package/src/services/acme.ts +1140 -0
- package/src/services/index.ts +2 -0
- package/src/traits.ts +46 -0
package/lib/jose.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOSE helpers for ACME — hand-written, WebCrypto only (no native code).
|
|
3
|
+
*
|
|
4
|
+
* - account keys (ES256 P-256 by default, RS256 accepted)
|
|
5
|
+
* - JWS in the flattened JSON serialization every ACME POST carries
|
|
6
|
+
* (RFC 8555 §6.2), including the HS256 External Account Binding
|
|
7
|
+
* - RFC 7638 JWK thumbprints, key authorizations and the DNS-01 TXT value
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import { JoseError } from "./errors.ts";
|
|
12
|
+
export type Algorithm = "ES256" | "RS256";
|
|
13
|
+
export interface Jwk {
|
|
14
|
+
readonly kty: string;
|
|
15
|
+
readonly crv?: string;
|
|
16
|
+
readonly x?: string;
|
|
17
|
+
readonly y?: string;
|
|
18
|
+
readonly n?: string;
|
|
19
|
+
readonly e?: string;
|
|
20
|
+
readonly d?: string;
|
|
21
|
+
readonly [key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
export declare const base64url: (bytes: Uint8Array | ArrayBuffer) => string;
|
|
24
|
+
export declare const base64urlDecode: (text: string) => Uint8Array;
|
|
25
|
+
/** Detect the JWS algorithm for a JWK. */
|
|
26
|
+
export declare const algorithmOf: (jwk: Jwk) => Algorithm;
|
|
27
|
+
/** Generate a fresh account key as a private JWK (JSON text, Redacted). */
|
|
28
|
+
export declare const generateAccountKey: (alg?: Algorithm) => Effect.Effect<Redacted.Redacted<string>>;
|
|
29
|
+
/** Parse a private JWK from its JSON text. */
|
|
30
|
+
export declare const parseJwk: (key: Redacted.Redacted<string> | string) => Effect.Effect<Jwk, JoseError>;
|
|
31
|
+
/** The public half of a JWK, with members in RFC 7638 canonical order. */
|
|
32
|
+
export declare const publicJwk: (jwk: Jwk) => Jwk;
|
|
33
|
+
/** RFC 7638 JWK thumbprint (base64url SHA-256 of the canonical public JWK). */
|
|
34
|
+
export declare const thumbprint: (jwk: Jwk) => Effect.Effect<string>;
|
|
35
|
+
/** `<token>.<thumbprint>` — what a challenge proves the client holds. */
|
|
36
|
+
export declare const keyAuthorization: (token: string, jwk: Jwk) => Effect.Effect<string>;
|
|
37
|
+
/** The TXT record value for a DNS-01 challenge: base64url(SHA-256(keyAuthorization)). */
|
|
38
|
+
export declare const dnsChallengeValue: (token: string, jwk: Jwk) => Effect.Effect<string>;
|
|
39
|
+
export interface FlattenedJws {
|
|
40
|
+
readonly protected: string;
|
|
41
|
+
readonly payload: string;
|
|
42
|
+
readonly signature: string;
|
|
43
|
+
}
|
|
44
|
+
export interface SignOptions {
|
|
45
|
+
readonly jwk: Jwk;
|
|
46
|
+
readonly url: string;
|
|
47
|
+
readonly nonce: string;
|
|
48
|
+
/** Sent as `kid` when known; otherwise the public JWK is embedded. */
|
|
49
|
+
readonly kid?: string | undefined;
|
|
50
|
+
/** `undefined` produces a POST-as-GET (empty payload). */
|
|
51
|
+
readonly payload?: unknown;
|
|
52
|
+
}
|
|
53
|
+
/** Sign an ACME request body (RFC 8555 §6.2) with the account key. */
|
|
54
|
+
export declare const signRequest: (options: SignOptions) => Effect.Effect<FlattenedJws, JoseError>;
|
|
55
|
+
/**
|
|
56
|
+
* The External Account Binding JWS (RFC 8555 §7.3.4): HS256 over the
|
|
57
|
+
* account's public JWK, keyed by the CA-issued HMAC key, with the CA's key
|
|
58
|
+
* id as `kid`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const signExternalAccountBinding: (options: {
|
|
61
|
+
readonly jwk: Jwk;
|
|
62
|
+
readonly url: string;
|
|
63
|
+
readonly keyId: string;
|
|
64
|
+
readonly hmacKey: Redacted.Redacted<string> | string;
|
|
65
|
+
}) => Effect.Effect<FlattenedJws, JoseError>;
|
|
66
|
+
//# sourceMappingURL=jose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jose.d.ts","sourceRoot":"","sources":["../src/jose.ts"],"names":[],"mappings":"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"}
|
package/lib/jose.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JOSE helpers for ACME — hand-written, WebCrypto only (no native code).
|
|
3
|
+
*
|
|
4
|
+
* - account keys (ES256 P-256 by default, RS256 accepted)
|
|
5
|
+
* - JWS in the flattened JSON serialization every ACME POST carries
|
|
6
|
+
* (RFC 8555 §6.2), including the HS256 External Account Binding
|
|
7
|
+
* - RFC 7638 JWK thumbprints, key authorizations and the DNS-01 TXT value
|
|
8
|
+
*/
|
|
9
|
+
import * as Effect from "effect/Effect";
|
|
10
|
+
import * as Redacted from "effect/Redacted";
|
|
11
|
+
import { JoseError } from "./errors.js";
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
export const base64url = (bytes) => {
|
|
14
|
+
const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
15
|
+
let binary = "";
|
|
16
|
+
for (const byte of view)
|
|
17
|
+
binary += String.fromCharCode(byte);
|
|
18
|
+
return btoa(binary)
|
|
19
|
+
.replaceAll("+", "-")
|
|
20
|
+
.replaceAll("/", "_")
|
|
21
|
+
.replace(/=+$/, "");
|
|
22
|
+
};
|
|
23
|
+
export const base64urlDecode = (text) => {
|
|
24
|
+
const padded = text.replaceAll("-", "+").replaceAll("_", "/");
|
|
25
|
+
const binary = atob(padded + "=".repeat((4 - (padded.length % 4)) % 4));
|
|
26
|
+
const out = new Uint8Array(binary.length);
|
|
27
|
+
for (let i = 0; i < binary.length; i++)
|
|
28
|
+
out[i] = binary.charCodeAt(i);
|
|
29
|
+
return out;
|
|
30
|
+
};
|
|
31
|
+
const base64urlJson = (value) => base64url(encoder.encode(JSON.stringify(value)));
|
|
32
|
+
const sha256 = (data) => Effect.promise(async () => {
|
|
33
|
+
const bytes = typeof data === "string" ? encoder.encode(data) : data;
|
|
34
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
35
|
+
});
|
|
36
|
+
/** Detect the JWS algorithm for a JWK. */
|
|
37
|
+
export const algorithmOf = (jwk) => jwk.kty === "RSA" ? "RS256" : "ES256";
|
|
38
|
+
const importAlgorithm = (alg) => alg === "RS256"
|
|
39
|
+
? { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }
|
|
40
|
+
: { name: "ECDSA", namedCurve: "P-256" };
|
|
41
|
+
const signAlgorithm = (alg) => alg === "RS256"
|
|
42
|
+
? { name: "RSASSA-PKCS1-v1_5" }
|
|
43
|
+
: { name: "ECDSA", hash: "SHA-256" };
|
|
44
|
+
/** Generate a fresh account key as a private JWK (JSON text, Redacted). */
|
|
45
|
+
export const generateAccountKey = (alg = "ES256") => Effect.promise(async () => {
|
|
46
|
+
const pair = await crypto.subtle.generateKey(alg === "RS256"
|
|
47
|
+
? {
|
|
48
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
49
|
+
modulusLength: 2048,
|
|
50
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
51
|
+
hash: "SHA-256",
|
|
52
|
+
}
|
|
53
|
+
: { name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
|
|
54
|
+
const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
55
|
+
return Redacted.make(JSON.stringify(jwk));
|
|
56
|
+
});
|
|
57
|
+
/** Parse a private JWK from its JSON text. */
|
|
58
|
+
export const parseJwk = (key) => Effect.try({
|
|
59
|
+
try: () => JSON.parse(Redacted.isRedacted(key) ? Redacted.value(key) : key),
|
|
60
|
+
catch: (cause) => new JoseError({
|
|
61
|
+
message: "The account key is not a JSON Web Key.",
|
|
62
|
+
cause,
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
/** The public half of a JWK, with members in RFC 7638 canonical order. */
|
|
66
|
+
export const publicJwk = (jwk) => jwk.kty === "RSA"
|
|
67
|
+
? { e: jwk.e, kty: "RSA", n: jwk.n }
|
|
68
|
+
: { crv: jwk.crv, kty: "EC", x: jwk.x, y: jwk.y };
|
|
69
|
+
/** RFC 7638 JWK thumbprint (base64url SHA-256 of the canonical public JWK). */
|
|
70
|
+
export const thumbprint = (jwk) => sha256(JSON.stringify(publicJwk(jwk))).pipe(Effect.map(base64url));
|
|
71
|
+
/** `<token>.<thumbprint>` — what a challenge proves the client holds. */
|
|
72
|
+
export const keyAuthorization = (token, jwk) => thumbprint(jwk).pipe(Effect.map((print) => `${token}.${print}`));
|
|
73
|
+
/** The TXT record value for a DNS-01 challenge: base64url(SHA-256(keyAuthorization)). */
|
|
74
|
+
export const dnsChallengeValue = (token, jwk) => keyAuthorization(token, jwk).pipe(Effect.flatMap(sha256), Effect.map(base64url));
|
|
75
|
+
/** Sign an ACME request body (RFC 8555 §6.2) with the account key. */
|
|
76
|
+
export const signRequest = (options) => Effect.tryPromise({
|
|
77
|
+
try: async () => {
|
|
78
|
+
const alg = algorithmOf(options.jwk);
|
|
79
|
+
const header = {
|
|
80
|
+
alg,
|
|
81
|
+
nonce: options.nonce,
|
|
82
|
+
url: options.url,
|
|
83
|
+
...(options.kid !== undefined
|
|
84
|
+
? { kid: options.kid }
|
|
85
|
+
: { jwk: publicJwk(options.jwk) }),
|
|
86
|
+
};
|
|
87
|
+
const protectedB64 = base64urlJson(header);
|
|
88
|
+
const payloadB64 = options.payload === undefined ? "" : base64urlJson(options.payload);
|
|
89
|
+
const key = await crypto.subtle.importKey("jwk", options.jwk, importAlgorithm(alg), false, ["sign"]);
|
|
90
|
+
const signature = await crypto.subtle.sign(signAlgorithm(alg), key, encoder.encode(`${protectedB64}.${payloadB64}`));
|
|
91
|
+
return {
|
|
92
|
+
protected: protectedB64,
|
|
93
|
+
payload: payloadB64,
|
|
94
|
+
signature: base64url(signature),
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
catch: (cause) => new JoseError({ message: "Signing the ACME request failed.", cause }),
|
|
98
|
+
});
|
|
99
|
+
/**
|
|
100
|
+
* The External Account Binding JWS (RFC 8555 §7.3.4): HS256 over the
|
|
101
|
+
* account's public JWK, keyed by the CA-issued HMAC key, with the CA's key
|
|
102
|
+
* id as `kid`.
|
|
103
|
+
*/
|
|
104
|
+
export const signExternalAccountBinding = (options) => Effect.tryPromise({
|
|
105
|
+
try: async () => {
|
|
106
|
+
const raw = Redacted.isRedacted(options.hmacKey)
|
|
107
|
+
? Redacted.value(options.hmacKey)
|
|
108
|
+
: options.hmacKey;
|
|
109
|
+
const key = await crypto.subtle.importKey("raw", base64urlDecode(raw), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
110
|
+
const protectedB64 = base64urlJson({
|
|
111
|
+
alg: "HS256",
|
|
112
|
+
kid: options.keyId,
|
|
113
|
+
url: options.url,
|
|
114
|
+
});
|
|
115
|
+
const payloadB64 = base64urlJson(publicJwk(options.jwk));
|
|
116
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${protectedB64}.${payloadB64}`));
|
|
117
|
+
return {
|
|
118
|
+
protected: protectedB64,
|
|
119
|
+
payload: payloadB64,
|
|
120
|
+
signature: base64url(signature),
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
catch: (cause) => new JoseError({
|
|
124
|
+
message: "Signing the External Account Binding failed.",
|
|
125
|
+
cause,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=jose.js.map
|
package/lib/jose.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jose.js","sourceRoot":"","sources":["../src/jose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as Layer from "effect/Layer";
|
|
2
|
+
import * as Redacted from "effect/Redacted";
|
|
3
|
+
import type * as AST from "effect/SchemaAST";
|
|
4
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
5
|
+
import type * as HttpClientError from "effect/http/HttpClientError";
|
|
6
|
+
import * as API from "@distilled.cloud/core/api";
|
|
7
|
+
import { type ConfigError } from "@distilled.cloud/core/errors";
|
|
8
|
+
import { Credentials } from "./credentials.ts";
|
|
9
|
+
import { type DefaultErrors } from "./errors.ts";
|
|
10
|
+
/**
|
|
11
|
+
* Error channel shared by every generated ACME operation. Generated service
|
|
12
|
+
* files annotate operations with `API.OperationMethod<I, O, AcmeOpError,
|
|
13
|
+
* AcmeOpContext>` explicitly so the compiler never infers these back out of
|
|
14
|
+
* the schema generics.
|
|
15
|
+
*/
|
|
16
|
+
export type AcmeOpError = DefaultErrors | ConfigError | HttpClientError.HttpClientError;
|
|
17
|
+
/** Context (requirements) shared by every generated ACME operation. */
|
|
18
|
+
export type AcmeOpContext = Credentials | HttpClient.HttpClient;
|
|
19
|
+
export declare const AcmeProtocol: Layer.Layer<API.Protocol>;
|
|
20
|
+
/** Forget cached directories and nonces (tests, CA switches). */
|
|
21
|
+
export declare const resetProtocolCaches: () => void;
|
|
22
|
+
/** @internal re-exported for tests */
|
|
23
|
+
export declare const _internal: {
|
|
24
|
+
getAnn: (ast: AST.AST, symbol: symbol) => unknown;
|
|
25
|
+
Redacted: typeof Redacted;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,QAAQ,MAAM,iBAAiB,CAAC;AAG5C,OAAO,KAAK,KAAK,GAAG,MAAM,kBAAkB,CAAC;AAC7C,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,KAAK,eAAe,MAAM,6BAA6B,CAAC;AAGpE,OAAO,KAAK,GAAG,MAAM,2BAA2B,CAAC;AAajD,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,8BAA8B,CAAC;AAKtC,OAAO,EAAE,WAAW,EAAe,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAQrB;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,aAAa,GACb,WAAW,GACX,eAAe,CAAC,eAAe,CAAC;AAEpC,uEAAuE;AACvE,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC;AA4dhE,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAOlD,CAAC;AAEF,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,QAAO,IAGtC,CAAC;AAEF,sCAAsC;AACtC,eAAO,MAAM,SAAS;;;CAAuB,CAAC"}
|
package/lib/protocol.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AcmeProtocol — hand-written.
|
|
3
|
+
*
|
|
4
|
+
* ACME (RFC 8555) is JSON over HTTPS with two twists the generic REST
|
|
5
|
+
* protocol cannot express:
|
|
6
|
+
*
|
|
7
|
+
* signing: every POST body is a flattened JWS (RFC 8555 §6.2) signed by
|
|
8
|
+
* the account key from `Credentials`, carrying a fresh
|
|
9
|
+
* anti-replay nonce and the request URL in its protected
|
|
10
|
+
* header. `newAccount` embeds the public JWK; every later
|
|
11
|
+
* request references the account URL as `kid`. A CA-issued
|
|
12
|
+
* External Account Binding is attached to `newAccount` when
|
|
13
|
+
* the credentials carry one. Reads are "POST-as-GET" (empty
|
|
14
|
+
* payload).
|
|
15
|
+
*
|
|
16
|
+
* addressing: the provider endpoint is the CA's **directory URL**. The
|
|
17
|
+
* protocol fetches the directory once per CA and resolves
|
|
18
|
+
* `newNonce`/`newAccount`/`newOrder`/`revokeCert` from it;
|
|
19
|
+
* every other operation names its resource by the absolute
|
|
20
|
+
* URL the CA returned (`url` label).
|
|
21
|
+
*
|
|
22
|
+
* Responses are JSON; `Location` (account/order URL), `Replay-Nonce` and
|
|
23
|
+
* `Link` headers are folded into declared output members. Failures are
|
|
24
|
+
* RFC 7807 problem documents matched on their `type` URN against the
|
|
25
|
+
* operation's typed error classes; an unmatched URN is `UnknownAcmeError`.
|
|
26
|
+
*/
|
|
27
|
+
import { isStrict, validateResponse, } from "@distilled.cloud/core/response-validation";
|
|
28
|
+
import * as Effect from "effect/Effect";
|
|
29
|
+
import * as Layer from "effect/Layer";
|
|
30
|
+
import * as Redacted from "effect/Redacted";
|
|
31
|
+
import * as Schema from "effect/Schema";
|
|
32
|
+
import * as HttpClient from "effect/http/HttpClient";
|
|
33
|
+
import * as HttpClientRequest from "effect/http/HttpClientRequest";
|
|
34
|
+
import * as API from "@distilled.cloud/core/api";
|
|
35
|
+
import { getAnn, getProps, hasPropAnn, mapKeys, } from "@distilled.cloud/core/protocol-http";
|
|
36
|
+
import { unwrapRedactedDeep } from "@distilled.cloud/core/protocol-rest";
|
|
37
|
+
import { getErrorMatchers, httpSymbol, labelSymbol, } from "@distilled.cloud/core/trait";
|
|
38
|
+
import { HTTP_STATUS_MAP, InternalServerError, } from "@distilled.cloud/core/errors";
|
|
39
|
+
import { parseRetryAfter, parseRetryAfterForStatus, } from "@distilled.cloud/core/retry-after";
|
|
40
|
+
import { Credentials } from "./credentials.js";
|
|
41
|
+
import { AcmeParseError, DirectoryMissingResource, JoseError, UnknownAcmeError, } from "./errors.js";
|
|
42
|
+
import { parseJwk, signExternalAccountBinding, signRequest, } from "./jose.js";
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Directory + nonce caches (per CA, process-wide)
|
|
45
|
+
// =============================================================================
|
|
46
|
+
const DirectorySchema = Schema.Struct({
|
|
47
|
+
newNonce: Schema.String,
|
|
48
|
+
newAccount: Schema.String,
|
|
49
|
+
newOrder: Schema.String,
|
|
50
|
+
revokeCert: Schema.String,
|
|
51
|
+
newAuthz: Schema.optional(Schema.String),
|
|
52
|
+
keyChange: Schema.optional(Schema.String),
|
|
53
|
+
meta: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
|
|
54
|
+
});
|
|
55
|
+
const directories = new Map();
|
|
56
|
+
/** The last unused `Replay-Nonce` per directory URL. Nonces are single-use. */
|
|
57
|
+
const nonces = new Map();
|
|
58
|
+
// HTTP request copies preserve the body identity; entries live only with a request.
|
|
59
|
+
const signedRequests = new WeakMap();
|
|
60
|
+
const resolveCredentials = Effect.gen(function* () {
|
|
61
|
+
const resolve = yield* Credentials;
|
|
62
|
+
return yield* resolve;
|
|
63
|
+
});
|
|
64
|
+
const fetchDirectory = (directoryUrl) => Effect.gen(function* () {
|
|
65
|
+
const cached = directories.get(directoryUrl);
|
|
66
|
+
if (cached)
|
|
67
|
+
return cached;
|
|
68
|
+
const client = yield* HttpClient.HttpClient;
|
|
69
|
+
const response = yield* client.get(directoryUrl);
|
|
70
|
+
rememberNonce(directoryUrl, response);
|
|
71
|
+
if (response.status >= 400) {
|
|
72
|
+
const text = yield* response.text;
|
|
73
|
+
return yield* fail(new UnknownAcmeError({
|
|
74
|
+
message: `GET ${directoryUrl} answered ${response.status}`,
|
|
75
|
+
status: response.status,
|
|
76
|
+
body: Redacted.make(text),
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
const text = yield* response.text;
|
|
80
|
+
const json = yield* parseJson(text);
|
|
81
|
+
const directory = yield* Schema.decodeUnknownEffect(DirectorySchema)(json).pipe(Effect.mapError(() => parseError("Invalid ACME directory response")));
|
|
82
|
+
directories.set(directoryUrl, directory);
|
|
83
|
+
return directory;
|
|
84
|
+
});
|
|
85
|
+
const rememberNonce = (directoryUrl, response) => {
|
|
86
|
+
const nonce = response.headers["replay-nonce"];
|
|
87
|
+
if (typeof nonce === "string" && nonce.length > 0) {
|
|
88
|
+
nonces.set(directoryUrl, nonce);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** Take the cached nonce for this CA, or fetch a fresh one from `newNonce`. */
|
|
92
|
+
const takeNonce = (directoryUrl, directory) => Effect.gen(function* () {
|
|
93
|
+
const cached = nonces.get(directoryUrl);
|
|
94
|
+
if (cached !== undefined) {
|
|
95
|
+
nonces.delete(directoryUrl);
|
|
96
|
+
return cached;
|
|
97
|
+
}
|
|
98
|
+
const client = yield* HttpClient.HttpClient;
|
|
99
|
+
const response = yield* client.execute(HttpClientRequest.head(directory.newNonce));
|
|
100
|
+
const nonce = response.headers["replay-nonce"];
|
|
101
|
+
if (typeof nonce !== "string" || nonce.length === 0) {
|
|
102
|
+
return yield* fail(new UnknownAcmeError({
|
|
103
|
+
message: `HEAD ${directory.newNonce} returned no Replay-Nonce header`,
|
|
104
|
+
status: response.status,
|
|
105
|
+
body: undefined,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
return nonce;
|
|
109
|
+
});
|
|
110
|
+
// =============================================================================
|
|
111
|
+
// Encode
|
|
112
|
+
// =============================================================================
|
|
113
|
+
/**
|
|
114
|
+
* The generated operations carry no name at runtime; the input's `T.Http`
|
|
115
|
+
* uri (from the Smithy model) identifies which ACME resource an
|
|
116
|
+
* operation addresses.
|
|
117
|
+
*/
|
|
118
|
+
const OPERATIONS_BY_URI = {
|
|
119
|
+
"/directory": "GetDirectory",
|
|
120
|
+
"/newNonce": "NewNonce",
|
|
121
|
+
"/newAccount": "NewAccount",
|
|
122
|
+
"/account/{url}": "UpdateAccount",
|
|
123
|
+
"/newOrder": "NewOrder",
|
|
124
|
+
"/order/{url}": "GetOrder",
|
|
125
|
+
"/finalize/{url}": "FinalizeOrder",
|
|
126
|
+
"/authz/{url}": "GetAuthorization",
|
|
127
|
+
"/authz/{url}/deactivate": "DeactivateAuthorization",
|
|
128
|
+
"/challenge/{url}": "RespondChallenge",
|
|
129
|
+
"/cert/{url}": "DownloadCertificate",
|
|
130
|
+
"/revokeCert": "RevokeCertificate",
|
|
131
|
+
};
|
|
132
|
+
const operationOf = (inputAst) => {
|
|
133
|
+
const http = getAnn(inputAst, httpSymbol);
|
|
134
|
+
return OPERATIONS_BY_URI[http?.uri ?? ""] ?? "";
|
|
135
|
+
};
|
|
136
|
+
/** Operations resolved from the directory rather than an input `url`. */
|
|
137
|
+
const DIRECTORY_OPERATIONS = {
|
|
138
|
+
NewNonce: "newNonce",
|
|
139
|
+
NewAccount: "newAccount",
|
|
140
|
+
NewOrder: "newOrder",
|
|
141
|
+
RevokeCertificate: "revokeCert",
|
|
142
|
+
};
|
|
143
|
+
/** Operations whose payload is the empty POST-as-GET. */
|
|
144
|
+
const POST_AS_GET = new Set([
|
|
145
|
+
"GetOrder",
|
|
146
|
+
"GetAuthorization",
|
|
147
|
+
"DownloadCertificate",
|
|
148
|
+
]);
|
|
149
|
+
const JOSE_ACCEPT = "application/json, application/pem-certificate-chain";
|
|
150
|
+
const encode = ({ input, inputAst, }) => Effect.gen(function* () {
|
|
151
|
+
const creds = yield* resolveCredentials;
|
|
152
|
+
const operation = operationOf(inputAst);
|
|
153
|
+
if (operation === "") {
|
|
154
|
+
return yield* fail(new JoseError({
|
|
155
|
+
message: "The operation's input carries no known ACME `T.Http` uri.",
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
if (operation === "GetDirectory") {
|
|
159
|
+
return HttpClientRequest.get(creds.directoryUrl).pipe(HttpClientRequest.setHeader("Accept", "application/json"));
|
|
160
|
+
}
|
|
161
|
+
const directory = yield* fetchDirectory(creds.directoryUrl);
|
|
162
|
+
if (operation === "NewNonce") {
|
|
163
|
+
return HttpClientRequest.head(directory.newNonce);
|
|
164
|
+
}
|
|
165
|
+
// Resolve the request URL: from the directory for the well-known
|
|
166
|
+
// resources, else the absolute `url` label the CA handed back.
|
|
167
|
+
const inputObj = (unwrapRedactedDeep(input) ?? {});
|
|
168
|
+
const directoryKey = DIRECTORY_OPERATIONS[operation];
|
|
169
|
+
let url;
|
|
170
|
+
if (directoryKey !== undefined) {
|
|
171
|
+
const resolved = directory[directoryKey];
|
|
172
|
+
if (typeof resolved !== "string") {
|
|
173
|
+
return yield* fail(new DirectoryMissingResource({
|
|
174
|
+
resource: directoryKey,
|
|
175
|
+
directoryUrl: creds.directoryUrl,
|
|
176
|
+
}));
|
|
177
|
+
}
|
|
178
|
+
url = resolved;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
const label = getProps(inputAst).find((p) => hasPropAnn(p, labelSymbol));
|
|
182
|
+
const value = label ? inputObj[String(label.name)] : undefined;
|
|
183
|
+
if (typeof value !== "string") {
|
|
184
|
+
return yield* fail(new JoseError({
|
|
185
|
+
message: `${operation} needs the resource URL the CA returned (\`url\`).`,
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
url = value;
|
|
189
|
+
}
|
|
190
|
+
// The payload: every input member that isn't the URL label, or the
|
|
191
|
+
// empty POST-as-GET for reads.
|
|
192
|
+
let payload;
|
|
193
|
+
if (!POST_AS_GET.has(operation)) {
|
|
194
|
+
const body = {};
|
|
195
|
+
for (const prop of getProps(inputAst)) {
|
|
196
|
+
if (hasPropAnn(prop, labelSymbol))
|
|
197
|
+
continue;
|
|
198
|
+
const key = String(prop.name);
|
|
199
|
+
if (inputObj[key] !== undefined)
|
|
200
|
+
body[key] = inputObj[key];
|
|
201
|
+
}
|
|
202
|
+
payload = body;
|
|
203
|
+
}
|
|
204
|
+
const jwk = yield* parseJwk(creds.accountKey);
|
|
205
|
+
// `newAccount` proves the key by embedding it; a CA that requires an
|
|
206
|
+
// External Account Binding gets it from the credentials unless the
|
|
207
|
+
// caller built one.
|
|
208
|
+
const embedKey = operation === "NewAccount" || creds.accountUrl === undefined;
|
|
209
|
+
if (operation === "NewAccount" &&
|
|
210
|
+
creds.externalAccountBinding !== undefined &&
|
|
211
|
+
payload.externalAccountBinding === undefined) {
|
|
212
|
+
payload.externalAccountBinding =
|
|
213
|
+
yield* signExternalAccountBinding({
|
|
214
|
+
jwk,
|
|
215
|
+
url,
|
|
216
|
+
keyId: creds.externalAccountBinding.keyId,
|
|
217
|
+
hmacKey: creds.externalAccountBinding.hmacKey,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const nonce = yield* takeNonce(creds.directoryUrl, directory);
|
|
221
|
+
const signing = {
|
|
222
|
+
jwk,
|
|
223
|
+
url,
|
|
224
|
+
kid: embedKey ? undefined : creds.accountUrl,
|
|
225
|
+
payload,
|
|
226
|
+
directoryUrl: creds.directoryUrl,
|
|
227
|
+
};
|
|
228
|
+
const request = yield* signedRequest(signing, nonce);
|
|
229
|
+
signedRequests.set(request.body, signing);
|
|
230
|
+
return request;
|
|
231
|
+
});
|
|
232
|
+
const signedRequest = (signing, nonce) => Effect.gen(function* () {
|
|
233
|
+
const jws = yield* signRequest({ ...signing, nonce });
|
|
234
|
+
return HttpClientRequest.post(signing.url).pipe(HttpClientRequest.setHeader("Accept", JOSE_ACCEPT), HttpClientRequest.bodyText(JSON.stringify(jws), "application/jose+json"));
|
|
235
|
+
});
|
|
236
|
+
const isProblem = (value) => isObject(value) && typeof value.type === "string";
|
|
237
|
+
/**
|
|
238
|
+
* Match a problem document against the operation's typed error classes by
|
|
239
|
+
* its `type` URN (the matcher's `message` rule is applied to the URN, so a
|
|
240
|
+
* patch writes `{ "message": { "matches": "^urn:ietf:params:acme:error:badNonce$" } }`).
|
|
241
|
+
*/
|
|
242
|
+
const matchProblem = (errorClasses, status, problem, headers) => {
|
|
243
|
+
const urn = problem.type ?? "";
|
|
244
|
+
let best;
|
|
245
|
+
for (const cls of errorClasses) {
|
|
246
|
+
const matchers = getErrorMatchers(cls);
|
|
247
|
+
if (!matchers)
|
|
248
|
+
continue;
|
|
249
|
+
for (const m of matchers) {
|
|
250
|
+
if (m.status !== undefined && m.status !== status)
|
|
251
|
+
continue;
|
|
252
|
+
if (m.code !== undefined && m.code !== status)
|
|
253
|
+
continue;
|
|
254
|
+
const rule = m.message;
|
|
255
|
+
let hit = false;
|
|
256
|
+
if (rule === undefined)
|
|
257
|
+
hit = true;
|
|
258
|
+
else if (typeof rule === "string")
|
|
259
|
+
hit = rule === urn;
|
|
260
|
+
else if (rule.matches !== undefined)
|
|
261
|
+
hit = new RegExp(rule.matches).test(urn);
|
|
262
|
+
else if (rule.includes !== undefined)
|
|
263
|
+
hit = urn.includes(rule.includes);
|
|
264
|
+
if (!hit)
|
|
265
|
+
continue;
|
|
266
|
+
const specificity = (m.status !== undefined ? 1 : 0) + (rule !== undefined ? 2 : 0);
|
|
267
|
+
if (!best || specificity > best.specificity)
|
|
268
|
+
best = { cls, specificity };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (!best)
|
|
272
|
+
return undefined;
|
|
273
|
+
return new best.cls({
|
|
274
|
+
code: status,
|
|
275
|
+
message: problem.detail ?? urn,
|
|
276
|
+
type: urn,
|
|
277
|
+
detail: problem.detail,
|
|
278
|
+
subproblems: problem.subproblems,
|
|
279
|
+
...(urn === "urn:ietf:params:acme:error:rateLimited"
|
|
280
|
+
? { retryAfter: parseRetryAfter(headers) }
|
|
281
|
+
: {}),
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
const parseLinkAlternates = (link) => {
|
|
285
|
+
if (!link)
|
|
286
|
+
return [];
|
|
287
|
+
const out = [];
|
|
288
|
+
for (const part of link.split(",")) {
|
|
289
|
+
const match = part.match(/<([^>]+)>\s*;\s*rel="?alternate"?/);
|
|
290
|
+
if (match)
|
|
291
|
+
out.push(match[1]);
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
};
|
|
295
|
+
const decode = ({ response, outputAst, errors: errorClasses, }) => Effect.gen(function* () {
|
|
296
|
+
const signing = signedRequests.get(response.request.body);
|
|
297
|
+
signedRequests.delete(response.request.body);
|
|
298
|
+
const directoryUrl = signing?.directoryUrl ?? (yield* resolveCredentials).directoryUrl;
|
|
299
|
+
for (let retries = 0;; retries++) {
|
|
300
|
+
const headers = response.headers;
|
|
301
|
+
const status = response.status;
|
|
302
|
+
const isNewNonce = response.request.method === "HEAD";
|
|
303
|
+
const isCertificate = (headers["content-type"] ?? "").includes("pem-certificate-chain");
|
|
304
|
+
const text = isNewNonce ? "" : yield* response.text;
|
|
305
|
+
const json = isNewNonce || (isCertificate && status < 400)
|
|
306
|
+
? undefined
|
|
307
|
+
: yield* parseJson(text).pipe(
|
|
308
|
+
// A non-JSON 2xx fails only in strict mode; lenient returns
|
|
309
|
+
// the text as read.
|
|
310
|
+
Effect.catchTag("AcmeParseError", (error) => status >= 400
|
|
311
|
+
? Effect.succeed(undefined)
|
|
312
|
+
: Effect.flatMap(isStrict, (strict) => strict
|
|
313
|
+
? Effect.fail(error)
|
|
314
|
+
: Effect.succeed(text))));
|
|
315
|
+
const problem = status >= 400 && isProblem(json) ? json : undefined;
|
|
316
|
+
const badNonce = problem?.type === "urn:ietf:params:acme:error:badNonce";
|
|
317
|
+
if (badNonce) {
|
|
318
|
+
const nonce = headers["replay-nonce"];
|
|
319
|
+
// Rejection nonces belong only to this request, never to the shared cache.
|
|
320
|
+
if (signing && nonce && retries < 2) {
|
|
321
|
+
const client = yield* HttpClient.HttpClient;
|
|
322
|
+
response = yield* client.execute(yield* signedRequest(signing, nonce));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
rememberNonce(directoryUrl, response);
|
|
328
|
+
}
|
|
329
|
+
if (status >= 400) {
|
|
330
|
+
if (problem) {
|
|
331
|
+
const typed = matchProblem(errorClasses, status, problem, headers);
|
|
332
|
+
if (typed !== undefined)
|
|
333
|
+
return yield* fail(typed);
|
|
334
|
+
return yield* fail(new UnknownAcmeError({
|
|
335
|
+
type: problem.type,
|
|
336
|
+
detail: problem.detail,
|
|
337
|
+
subproblems: problem.subproblems,
|
|
338
|
+
message: problem.detail ?? problem.type,
|
|
339
|
+
status,
|
|
340
|
+
body: Redacted.make(json),
|
|
341
|
+
}));
|
|
342
|
+
}
|
|
343
|
+
const message = `HTTP ${status}`;
|
|
344
|
+
const StatusClass = HTTP_STATUS_MAP[status];
|
|
345
|
+
if (StatusClass) {
|
|
346
|
+
return yield* fail(new StatusClass({
|
|
347
|
+
message,
|
|
348
|
+
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
if (status >= 500) {
|
|
352
|
+
return yield* fail(new InternalServerError({
|
|
353
|
+
message,
|
|
354
|
+
retryAfter: parseRetryAfterForStatus(status, headers),
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
return yield* fail(new UnknownAcmeError({
|
|
358
|
+
message,
|
|
359
|
+
status,
|
|
360
|
+
body: Redacted.make(json ?? text),
|
|
361
|
+
}));
|
|
362
|
+
}
|
|
363
|
+
let body;
|
|
364
|
+
if (isNewNonce) {
|
|
365
|
+
body = { replayNonce: headers["replay-nonce"] || undefined };
|
|
366
|
+
}
|
|
367
|
+
else if (isCertificate) {
|
|
368
|
+
body = {
|
|
369
|
+
chain: text,
|
|
370
|
+
alternates: parseLinkAlternates(headers["link"]),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
else if (isObject(json)) {
|
|
374
|
+
body = json;
|
|
375
|
+
const location = headers["location"];
|
|
376
|
+
if (location)
|
|
377
|
+
body = { ...body, location };
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
// Lenient mode returns a non-object body as read.
|
|
381
|
+
if (!(yield* isStrict))
|
|
382
|
+
return json;
|
|
383
|
+
return yield* fail(parseError("Expected a JSON object"));
|
|
384
|
+
}
|
|
385
|
+
// Strict mode (core/response-validation) checks the output schema.
|
|
386
|
+
return yield* validateResponse(outputAst, mapKeys(outputAst, body, "decode"), () => parseError("Response does not match the output schema"));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
390
|
+
const parseError = (cause) => new AcmeParseError({
|
|
391
|
+
body: "[REDACTED]",
|
|
392
|
+
cause,
|
|
393
|
+
});
|
|
394
|
+
const parseJson = (text) => Effect.try({
|
|
395
|
+
try: () => (text.trim().length > 0 ? JSON.parse(text) : {}),
|
|
396
|
+
catch: () => parseError("Invalid JSON response"),
|
|
397
|
+
});
|
|
398
|
+
const fail = (error) => Effect.fail(error);
|
|
399
|
+
export const AcmeProtocol = Layer.succeed(API.Protocol, API.Protocol.of({
|
|
400
|
+
encode: (args) => encode(args),
|
|
401
|
+
decode: (args) => decode(args),
|
|
402
|
+
}));
|
|
403
|
+
/** Forget cached directories and nonces (tests, CA switches). */
|
|
404
|
+
export const resetProtocolCaches = () => {
|
|
405
|
+
directories.clear();
|
|
406
|
+
nonces.clear();
|
|
407
|
+
};
|
|
408
|
+
/** @internal re-exported for tests */
|
|
409
|
+
export const _internal = { getAnn, Redacted };
|
|
410
|
+
//# sourceMappingURL=protocol.js.map
|