Skip to content
LogoLogo

@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>;
OptionTypeDefaultDescription
domainstringThe signing domain (d=). U-labels are converted to A-labels.
selectorstringThe selector (s=): the key is published at <selector>._domainkey.<domain>.
privateKeystring | CryptoKey | JsonWebKeyAn 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=.
headersreadonly string[]the fields mailauth signs, from From to BIMI-SelectorThe 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.
identitystringThe agent or user identifier (i=), such as user@example.com or @mail.example.com.
nowanynew Date()The signature time (t=), as a Date or a Temporal.Instant.
expiresAtanyThe 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.digestCreates the body hash.
signalAbortSignalStops 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[]>;
OptionTypeDefaultDescription
resolverResolverLooks up the keys. Pass Dns.create() or node:dns/promises.
nowanynew Date()The time to check x= against, as a Date or a Temporal.Instant.
minKeyBitsnumber1024RSA keys shorter than this give policy (RFC 8301 section 3.2).
hasher((hash: Hash) => Hasher)buffers the canonicalized body for crypto.subtle.digestCreates the body hash.
signalAbortSignalStops 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;
};