@portable/dkim
Functions
Dkim.sign
Signs a message (RFC 6376, RFC 8463) and resolves to the DKIM-Signature field, ending in CRLF,
to put above the message's other fields. The key type sets the algorithm: rsa-sha256 or
ed25519-sha256. The signature always has t= and never l=.
Throws a TypeError or RangeError for invalid options, and Dkim.InvalidInputError for a
message without a From field.
async function sign(
message: Message,
options: sign.Options,
): Promise<string>;| Option | Type | Default | Description |
|---|---|---|---|
domain | string | The signing domain (d=). U-labels are converted to A-labels. | |
selector | string | The selector (s=): the key is published at <selector>._domainkey.<domain>. | |
privateKey | string | CryptoKey | JsonWebKey | An RSA key of at least 1024 bits or an Ed25519 key: a CryptoKey that can sign (RSASSA-PKCS1-v1_5 with SHA-256, or Ed25519), a PEM PRIVATE KEY (PKCS #8) or RSA PRIVATE KEY (PKCS #1), or a JWK. The key type sets a=. | |
headers | readonly string[] | the fields mailauth signs, from From to BIMI-Selector | The header fields to sign, matched without regard to case. Each field of a listed name is signed. A name listed n times appears in h= at least n times, so listing a name more times than the message has it over-signs it: a field of that name added later breaks the signature (RFC 6376 section 8.15). ['From', 'From', 'Subject'] over-signs a message's single From. |
canonicalization | "simple/simple" | "simple/relaxed" | "relaxed/simple" | "relaxed/relaxed" | 'relaxed/relaxed' | header/body. |
identity | string | The agent or user identifier (i=), such as user@example.com or @mail.example.com. | |
now | any | new Date() | The signature time (t=), as a Date or a Temporal.Instant. |
expiresAt | any | The expiry (x=), as a Date or a Temporal.Instant. It must be after now. | |
hasher | ((hash: Hash) => Hasher) | buffers the canonicalized body for crypto.subtle.digest | Creates the body hash. |
signal | AbortSignal | Stops reading the message, and rejects with signal.reason. |
import * as Dkim from '@portable/dkim';
const field = await Dkim.sign(message, {
domain: 'example.com',
selector: 'mail',
privateKey: env.DKIM_PRIVATE_KEY,
});
const signed = field + message;Dkim.verify
Verifies every DKIM-Signature field of a message (RFC 6376, RFC 8301, RFC 8463) and resolves to one result per field, in header order. A message without signatures gives an empty array. The body is read once, whatever the number of signatures, and keys are looked up while it is read.
Results follow RFC 8601: fail for a body or signature that doesn't match, policy for
rsa-sha1 and short RSA keys, neutral for a signature that can't be processed, permerror for
a missing, revoked or unusable key, and temperror for a DNS failure. The promise rejects only
for invalid options, a failing message stream or an aborted signal.
async function verify(
message: Message,
options: verify.Options,
): Promise<Result[]>;| Option | Type | Default | Description |
|---|---|---|---|
resolver | Resolver | Looks up the keys. Pass Dns.create() or node:dns/promises. | |
now | any | new Date() | The time to check x= against, as a Date or a Temporal.Instant. |
minKeyBits | number | 1024 | RSA keys shorter than this give policy (RFC 8301 section 3.2). |
hasher | ((hash: Hash) => Hasher) | buffers the canonicalized body for crypto.subtle.digest | Creates the body hash. |
signal | AbortSignal | Stops reading the message and the key lookups, and rejects with signal.reason. |
import * as Dns from '@portable/dns';
import * as Dkim from '@portable/dkim';
const results = await Dkim.verify(request.body!, {
resolver: Dns.create(),
signal: AbortSignal.timeout(10_000),
});
for (const r of results) {
console.log(
`dkim=${r.result} header.d=${r.domain} header.s=${r.selector}`,
);
}Errors
Dkim.InvalidInputError
Thrown by sign for a message it can't sign: one without a From field, which RFC 6376
section 5.4 requires every signature to cover.
class InvalidInputError extends Errors.InvalidInputError {
readonly kind: string;
}import * as Dkim from '@portable/dkim';
try {
await Dkim.sign('Subject: hi\r\n\r\nbody\r\n', {
domain,
selector,
privateKey,
});
} catch (error) {
if (error instanceof Dkim.InvalidInputError) {
console.log(error.message);
}
}Types
Dkim.Message
A message as sign and verify take it. Text is encoded as UTF-8.
type Message = ReadableStream<Uint8Array> | Uint8Array | string;Dkim.Resolver
The DNS lookup DKIM needs: a Dns.Resolver from @portable/dns, node:dns/promises, or any
object with resolveTxt. verify treats a rejection with code ENOTFOUND, ENODATA or
EBADNAME as no key, and any other rejection as a temporary DNS error.
type Resolver = Pick<Dns.Resolver>;Dkim.Result
The outcome of one DKIM-Signature field.
type Result = {
result: Result.Code;
domain: string;
selector: string;
algorithm: string;
identity: string;
signature: string;
reason?: string | undefined;
cause?: unknown;
testing: boolean;
bodyLength?: number | undefined;
unsignedBodyBytes?: number | undefined;
};