Skip to content
LogoLogo

@portable/arc

Functions

Arc.seal

Adds an ARC set to a message (RFC 8617 section 5.1) and resolves to its three fields, ending in CRLF, to put above the message's other fields: ARC-Seal, ARC-Message-Signature and ARC-Authentication-Results. The new set's instance is one above the highest instance on the message. The ARC-Message-Signature uses relaxed/relaxed canonicalization, and with cv=fail the seal covers only the new set (section 5.1.2).

Throws a TypeError or RangeError for invalid options, such as an authResults with a line break that isn't folding, and Arc.InvalidInputError for a message that must not be sealed.

async function seal(
  message: Message,
  options: seal.Options,
): Promise<string>;
OptionTypeDefaultDescription
domainstringThe sealing domain (d= of both signatures). U-labels are converted to A-labels.
selectorstringThe selector (s=): the public key is published at <selector>._domainkey.<domain>.
privateKeystring | CryptoKey | JsonWebKeyAn RSA key of at least 1024 bits or an Ed25519 key, in any form Dkim.sign takes: a CryptoKey, a PEM PRIVATE KEY or RSA PRIVATE KEY, or a JWK. The key type sets a=.
authResultsstringThe ARC-Authentication-Results payload: this server's authserv-id and the results it found, such as mx.example.com; spf=pass smtp.mailfrom=example.org. It may be folded with CRLF and whitespace.
chainValidationResult.Code'none' for a message without themThe cv= value: the result of Arc.verify for this message. Required when the message already has ARC fields.
headersreadonly string[]DKIM-Signature, Delivered-To and the fields Dkim.sign signsThe fields the ARC-Message-Signature signs, as in Dkim.sign. ARC fields and Authentication-Results are never signed (RFC 8617 section 4.1.2).
nowanynew Date()The signature time (t=), as a Date or a Temporal.Instant.
hasher((hash: Dkim.Hash) => Dkim.Hasher)buffers the body for crypto.subtle.digestCreates the body hash, as in Dkim.sign.
signalAbortSignalStops reading the message, and rejects with signal.reason.
import * as Arc from '@portable/arc';
 
const arc = await Arc.verify(message, { resolver });
const fields = await Arc.seal(message, {
  domain: 'lists.example.org',
  selector: 'arc',
  privateKey: env.ARC_PRIVATE_KEY,
  authResults: `lists.example.org; arc=${arc.result}; spf=pass smtp.mailfrom=example.com`,
  chainValidation: arc.result,
});
const forwarded = fields + message;

Arc.verify

Validates the ARC chain of a message (RFC 8617 section 5.2) and resolves to pass, fail or none. It checks every set's structure, the newest ARC-Message-Signature and every ARC-Seal, with the RFC 6376 tag syntax, RFC 8301 algorithms and Ed25519 seals (RFC 8463). The body is read once, and the keys are looked up while it is read.

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
resolverDkim.ResolverLooks up the keys. Pass Dns.create() or node:dns/promises.
nowanynew Date()The time to check the ARC-Message-Signature's x= against, as a Date or a Temporal.Instant.
minKeyBitsnumber1024RSA keys shorter than this fail the chain (RFC 8301 section 3.2).
hasher((hash: Dkim.Hash) => Dkim.Hasher)buffers the body for crypto.subtle.digestCreates the body hash, as in Dkim.verify.
signalAbortSignalStops reading the message and the key lookups, and rejects with signal.reason.
import * as Arc from '@portable/arc';
import * as Dns from '@portable/dns';
 
const arc = await Arc.verify(message, { resolver: Dns.create() });
console.log(`arc=${arc.result}${arc.reason ? ` (${arc.reason})` : ''}`);

Errors

Arc.InvalidInputError

Thrown by seal for a message it must not seal: one without a From field, one whose newest ARC-Seal says cv=fail (RFC 8617 section 5.1 step 2), one that already has 50 ARC sets, or a cv=pass seal over a chain whose sets can't be read.

class InvalidInputError extends Errors.InvalidInputError {
  readonly kind: string;
}
import * as Arc from '@portable/arc';
 
try {
  headers = await Arc.seal(message, {
    domain,
    selector,
    privateKey,
    authResults,
    chainValidation,
  });
} catch (error) {
  if (!(error instanceof Arc.InvalidInputError)) {
    throw error;
  }
}

Types

Arc.Resolver

The DNS lookup ARC needs: a Dns.Resolver from @portable/dns, node:dns/promises, or any object with resolveTxt. It is the same as Dkim.Resolver.

type Resolver = Dkim.Resolver;

Arc.Result

The outcome of validating a message's ARC chain.

type Result = {
  result: Result.Code;
  instance: number;
  reason?: string | undefined;
  cause?: unknown;
  sets: readonly Result.Set[];
};