Email Believability Index

Email Believability Index (EBI) Specification — v0.11.0

Status: Draft

Last Updated: 2026-01-28


Overview

EBI provides a structured, machine-readable assessment of email authentication that goes beyond pass/fail verdicts. It quantifies risk, flags vulnerabilities, and provides the evidence trail needed for informed decisions.

Email authentication mechanisms (DKIM, SPF, DMARC) are powerful but their results hide crucial nuance. A DKIM “pass” doesn’t tell you whether the signature covers the entire body, whether the signing domain matches the From address, or whether the domain enforces DMARC policy. EBI surfaces this information.


Prerequisite Reading

Topic What It Does Specification
DKIM Cryptographically signs messages to prove integrity and sender domain RFC 6376
SPF Declares which servers may send mail for a domain RFC 7208
DMARC Ties DKIM and SPF together with policy enforcement RFC 7489
ARC Preserves authentication results across forwarding hops RFC 8617
Authentication-Results Standard header format for recording auth results RFC 8601

Email Authentication Summary

DKIM computes a cryptographic signature over message headers and body, embedding the signature in a DKIM-Signature header. Recipients verify against a public key in DNS. A valid signature proves the message hasn’t been tampered with and was signed by someone with access to the domain’s private key.

SPF publishes a DNS record declaring which IP addresses may send mail for a domain. Receiving servers check whether the sending IP is authorized.

DMARC requires that either DKIM or SPF passes and that the authenticated domain aligns with the visible From address. Without DMARC, an attacker could pass SPF using their own domain while spoofing a different From address.

ARC handles forwarding. When an email passes through a mailing list, the original DKIM signature often breaks. ARC creates a chain of custody: each server signs a statement recording authentication results at the time of receipt. If the ARC chain is intact, recipients can trust the original authentication results.


EBI Analysis Depth

Traditional checks report pass/fail. EBI reports:

These details surface as findings with severity ratings and recommendations.


Static .eml Limitations

When analyzing .eml files:

Mechanism Verifiable? Reason
DKIM Usually Signature in headers, key in DNS
SPF No Envelope data not in .eml
DMARC Partial Alignment yes, policy via DNS
ARC Yes Full chain in headers

EBI falls back to ARC chains and Authentication-Results headers when direct verification isn’t possible.


Analysis Modes

EBI operates in two distinct modes with different trust models:

Mode Context SPF Verifiable Envelope Available Primary Use
LIVE MTA integration Yes Yes Real-time filtering decisions
FORENSIC Static .eml analysis Via ARC only No Post-incident investigation

Implementations MUST set metadata.analysis.mode appropriately. The mode affects:


Design Goals

EBI is:


1. Technical Specification

1.1 What is EBI?

EBI is a structured JSON response summarizing email authenticity and integrity by analyzing:

EBI surfaces edge cases and vulnerabilities (weak DKIM canonicalization, partial body signing, relaxed alignment), allowing downstream systems to assess risk beyond pass/fail.

1.2 Analogy

EBI is analogous to OCSP for email authentication: a standardized response format conveying a verdict and supporting evidence.

1.3 Non-goals

1.4 Static .eml Limitations

When analyzing static .eml files:


2. Response Schema

2.1 Top-level Object

An EBI response MUST be a JSON object with the following structure.

{
  "ebi_version": "0.8",
  "request_id": "string",
  "timestamp": "RFC3339 string",
  "message_id": "string | null",

  "subject": "string | null",
  "from": "string | null",
  "from_display_name": "string | null",
  "to": ["string"],

  "request_context": { /* RequestContextObject */ },

  "verdict": { /* VerdictObject */ },
  "score": { /* ScoreObject */ },

  "dkim": { /* DKIMObject */ },
  "spf": { /* SPFObject */ },
  "dmarc": { /* DMARCObject */ },
  "arc": { /* ARCObject | null */ },

  "header_analysis": { /* HeaderAnalysisObject | null */ },

  "domain_details": { /* DomainDetailsObject | null */ },

  "findings": [ /* FindingObject[] */ ],

  "metadata": { /* MetadataObject */ }
}

2.2 Field Reference

Field Type Required Description
ebi_version string Yes Spec version. MUST be "0.8" for this spec.
request_id string Yes A unique ID for correlation across systems.
timestamp string Yes RFC3339 timestamp when analysis was completed.
message_id string | null No Message-ID header from the evaluated email.
subject string | null No Subject line (optional).
from string | null No Header From email address (optional).
from_display_name string | null No Display name portion of From header (optional).
to string[] No Header To list (optional).
request_context object Yes Context about the analysis request submission.
verdict object Yes High-level believability verdict and explanation.
score object Yes* Numeric score derived from verdict + findings. Required unless analysis is impossible.
dkim object Yes DKIM authentication results and signature details.
spf object Yes SPF authentication results and evaluation details.
dmarc object Yes DMARC evaluation results and alignment checks.
arc object | null No ARC chain validation (null if no ARC headers present).
header_analysis object | null No Analysis of header anomalies and display name spoofing indicators.
domain_details object | null No Domain threat intelligence including reputation, malware, and infrastructure analysis.
findings FindingObject[] Yes List of vulnerabilities, anomalies, and noteworthy conditions.
metadata object Yes Context for the analysis environment and optional raw evidence references.

* score MAY be null only when the system cannot compute any reliable verdict (e.g., missing message headers, parser failure). In that case verdict.status SHOULD be INCONCLUSIVE.


2.3 RequestContext Object

The request_context object provides metadata about how the analysis request was submitted, enabling report recipients to correlate reports with their submissions without exposing PII from the analyzed email.

2.3.1 RequestContextObject Schema

{
  "forwarder_email": "string",
  "received_at": "RFC3339 string"
}

2.3.2 RequestContext Field Reference

Field Type Required Description
forwarder_email string Yes Email address of the user who submitted the email for analysis.
received_at string Yes RFC3339 timestamp when the analysis request was received.

Privacy Note: The request_context contains only the submitter’s email address and submission timestamp. It does NOT include PII from the analyzed email beyond what is already present in the standard email headers (from, to, subject).


3. Verdict Object

3.1 VerdictObject Schema

{
  "status": "AUTHENTIC" | "PARTIAL" | "FAILED" | "UNSAFE" | "INCONCLUSIVE",
  "confidence": "HIGH" | "MEDIUM" | "LOW",
  "code": "string",
  "summary": "string",
  "explanation": "string | null",
  "flags": ["string"]
}

3.2 status Semantics

Status Meaning
AUTHENTIC Strong authentication and integrity; no high-risk weaknesses detected.
PARTIAL Some authentication succeeded but with missing signals or weak alignment.
FAILED Authentication failed or indicates possible spoofing.
UNSAFE Authentication passed but vulnerabilities or configuration weaknesses allow tampering or misattribution.
INCONCLUSIVE Not enough information to determine authenticity.

3.3 confidence

Confidence reflects the reliability of the verdict given available evidence.

Confidence When Used
HIGH All expected signals present and consistent
MEDIUM Some signals present but not all, or weak crypto used
LOW Unable to make a confident determination

3.4 Verdict Codes

The code field provides machine-readable detail about why a particular verdict was reached.

Code Status Description
ALL_PASS AUTHENTIC DKIM, SPF, and DMARC all passed
DKIM_PARTIAL_BODY_SIGNED UNSAFE DKIM l= tag present; body can be extended
DKIM_SIGNATURE_EXPIRED FAILED DKIM signature x= timestamp has passed
NO_AUTH_MECHANISMS FAILED No DKIM signatures present and sender has DKIM keys published (possible spoofing)
SENDER_NO_DKIM PARTIAL Sender domain has no DKIM keys published (poor email hygiene)
DMARC_FAIL FAILED DMARC alignment check failed (with enforcing policy)
DMARC_FAIL_POLICY_NONE PARTIAL DMARC alignment failed but domain publishes p=none
ALL_AUTH_FAIL FAILED Had signatures but all authentication mechanisms failed
WEAK_CRYPTO PARTIAL DKIM passed but uses deprecated SHA-1
DKIM_ONLY PARTIAL Only DKIM passed; SPF/DMARC absent or failed
SPF_ONLY PARTIAL Only SPF passed; DKIM/DMARC absent or failed
ARC_VOUCHED PARTIAL Valid ARC chain vouches for message despite expired signatures
UNKNOWN INCONCLUSIVE Unable to determine authenticity

3.5 Verdict Determination Priority

Implementations MUST evaluate verdict conditions in the following priority order (first match wins):

SPF Trust Model Note: When SPF is unverifiable (no valid ARC chain for static .eml files), SPF does NOT count for or against verdict determination. The rules below apply effectiveSpfPass which is true only if SPF is both verifiable AND passed.

  1. UNSAFE: Any DKIM signature has body_length.limited = true (l= tag present)
  2. FAILED: Any DKIM signature has expired (finding DKIM_SIGNATURE_EXPIRED present) AND message is NOT vouched by a valid ARC chain or Authentication-Results (see §3.6)
  3. PARTIAL: No DKIM signatures AND sender domain has no DKIM keys published (SENDER_NO_DKIM) (see §3.8)
  4. FAILED: No DKIM signatures AND sender domain HAS DKIM keys published (NO_AUTH_MECHANISMS) — possible spoofing
  5. FAILED: DMARC result is FAIL AND domain policy is NOT p=none (see §3.7)
  6. PARTIAL: DMARC result is FAIL AND domain policy is p=none, confidence=MEDIUM (DMARC_FAIL_POLICY_NONE)
  7. FAILED: DKIM failed AND SPF verifiably failed (ALL_AUTH_FAIL)
  8. PARTIAL: DKIM passed but uses weak cryptography (SHA-1), confidence=MEDIUM
  9. AUTHENTIC: DKIM passed AND DMARC passed AND (SPF passed OR SPF unverifiable)
  10. PARTIAL: DKIM passed (SPF unverifiable or failed), confidence=MEDIUM
  11. PARTIAL: SPF verifiably passed (but DKIM failed), confidence=MEDIUM
  12. INCONCLUSIVE: Otherwise, confidence=LOW

3.6 Expired Signatures with ARC/Authentication-Results

When a valid ARC chain exists, expired DKIM signatures SHOULD NOT force a FAILED verdict. The ARC chain serves as a trusted record of authentication state at time of receipt.

This applies in two scenarios:

  1. DKIM passed at origin: The ARC chain shows dkim=pass for the original hop. The signature was valid when received; subsequent expiry is irrelevant.

  2. DKIM absent at origin but other auth passed: The ARC chain shows dkim=none but spf=pass at the original hop. The expired signatures are from intermediaries (forwarding servers), not the original sender. The message was legitimately authenticated via SPF.

In either case, the DKIM_SIGNATURE_EXPIRED finding remains (severity HIGH) as evidence of the current signature state, but verdict determination continues to the next priority level.

Authentication-Results headers follow the same logic but are only trusted when no ARC chain exists, as they are more easily forged.

3.7 DMARC Failures with p=none Policy

When DMARC evaluation fails but the domain publishes p=none, implementations SHOULD NOT assign a FAILED verdict. The p=none policy is an explicit declaration that the domain does not enforce DMARC alignment.

Common legitimate scenarios for p=none:

In this case, the verdict SHOULD be PARTIAL with code DMARC_FAIL_POLICY_NONE. The DMARC_FAIL and DMARC_POLICY_NONE findings remain as evidence, but the message should not be treated as dangerous solely due to DMARC failure.

3.8 Distinguishing Poor Sender Hygiene from Spoofing

When a message lacks DKIM signatures, implementations SHOULD check whether the sender domain has DKIM keys published. This distinguishes between two fundamentally different scenarios:

Scenario A: Sender has no DKIM configured (poor email hygiene) - No DKIM signatures in message - DNS lookup shows no DKIM keys at common selectors - Verdict: PARTIAL with code SENDER_NO_DKIM - The sender simply hasn’t configured email authentication

Scenario B: Sender has DKIM but message lacks signatures (possible spoofing) - No DKIM signatures in message - DNS lookup shows DKIM keys ARE published for the domain - Verdict: FAILED with code NO_AUTH_MECHANISMS - A legitimate sender would sign; absence suggests spoofing

3.8.1 DKIM Key Discovery

Implementations SHOULD check these common selectors for DKIM keys:

The check queries {selector}._domainkey.{domain} for TXT records containing v=DKIM1 with a non-empty p= tag.

3.8.2 Limitations

DKIM key discovery is not exhaustive. Custom selectors may exist that are not in the common list. However, the major email platforms use well-known selectors, and the check provides a strong signal for the majority of legitimate senders.

If DNS lookup fails or times out, implementations SHOULD treat senderHasDkimKeys as undefined and fall back to the NO_AUTH_MECHANISMS verdict (assume possible spoofing as the safer default).


4. Score Object

4.1 Purpose

The score provides a single numeric projection of believability for UI display and policy thresholding.

The score is derived from verdict and findings. It is not a replacement for the richer EBI structure.

4.2 ScoreObject Schema

{
  "value": 0,
  "scale": { "min": 0, "max": 100 },
  "band": "EXCELLENT" | "GOOD" | "CAUTION" | "DANGEROUS" | "UNKNOWN",
  "method": "EBI_SCORE_V2",
  "components": {
    "base": 0,
    "finding_penalty": 0,
    "confidence_adjustment": 0
  },
  "notes": "string | null"
}

4.3 Bands

Band Range User Mental Model
EXCELLENT 90–100 Trust it
GOOD 80–89 Trust it
CAUTION 60–79 Raised eyebrow (70-79) to arms crossed (60-69)
DANGEROUS 0–59 Fail
UNKNOWN Used only when score is null.

4.4 Scoring Method: EBI_SCORE_V3

4.4.1 Design Rationale

EBI scores are calibrated for humans doing financial business who need to quickly assess email believability:

Most legitimate emails should score 75+. A score below 70 warrants investigation. A score below 60 is a red flag.

verdict.status base Rationale
AUTHENTIC 90 Full authentication passed, domain aligned. Trust zone.
PARTIAL 75 DKIM passed but incomplete alignment. Eyebrow territory.
INCONCLUSIVE 55 Cannot verify at all. Arms crossed.
FAILED 35 Authentication failed. Fail zone.
UNSAFE 25 Integrity compromised (l= tag). Hard fail regardless of other auth.

4.4.2 Calculation Steps

Step A: Base score from verdict.status

See table in §4.4.1.

Step B: Finding penalties

For each element in findings, subtract:

severity penalty
CRITICAL -25
HIGH -12
MEDIUM -6
LOW -2
INFO 0

Total finding penalty MUST be capped at -50.

Step C: Confidence adjustment

verdict.confidence adjustment
HIGH +5
MEDIUM 0
LOW -10

Step D: Clamp

score = clamp(base + finding_penalty + confidence_adjustment, 0, 100)

Step E: Band mapping

Band is determined from the ranges in §4.3.

4.4.3 Migration from EBI_SCORE_V2

Implementations upgrading from v0.8 SHOULD note:

Score thresholds for policy decisions should be recalibrated: - Old “trust” threshold of 80 → remains 80 - Old “review” threshold of 55 → consider raising to 70 - Old “block” threshold of 40 → consider raising to 60


5. Findings

5.1 FindingObject Schema

{
  "id": "string",
  "severity": "CRITICAL" | "HIGH" | "MEDIUM" | "LOW" | "INFO",
  "points": "number",
  "title": "string",
  "summary": "string",
  "details": "string | null",
  "evidence": {
    "type": "HEADER" | "DNS" | "DERIVED" | "BODY" | "OTHER",
    "key": "string | null",
    "value": "string | null"
  },
  "recommendation": "string | null"
}
Field Type Description
id string Stable identifier for rule-based matching (see §14)
severity enum CRITICAL, HIGH, MEDIUM, LOW, or INFO
points number Score impact of this finding (negative for penalties, 0 for INFO)
title string Short human-readable title
summary string One-sentence explanation
details string|null Extended explanation, may include technical background
evidence object Source of the finding
recommendation string|null Suggested action

Point Values by Severity: - CRITICAL: -25 - HIGH: -12 - MEDIUM: -6 - LOW: -2 - INFO: 0

5.2 Finding IDs

Finding IDs are stable strings suitable for rule-based policy. See §14 for the complete registry.


6. DKIM

6.1 DKIMObject Schema

{
  "result": "PASS" | "FAIL" | "NONE" | "TEMPERROR" | "PERMERROR",
  "from_domain_match": true,
  "domain": "string | null",
  "selector": "string | null",
  "signatures": [
    {
      "domain": "string",
      "selector": "string",
      "result": "PASS" | "FAIL" | "TEMPERROR" | "PERMERROR",
      "canonicalization": {
        "header": "simple" | "relaxed" | "unknown",
        "body": "simple" | "relaxed" | "unknown"
      },
      "body_length": {
        "limited": false,
        "value": null
      },
      "timestamp": "RFC3339 string | null",
      "expiry": "RFC3339 string | null",
      "hash_algo": "rsa-sha256" | "rsa-sha1" | "ed25519-sha256" | "unknown",
      "key_size": "number | null",
      "signed_headers": ["string"]
    }
  ]
}

6.2 Field Semantics

result

The effective DKIM result. May be derived from (in priority order):

  1. Direct verification: Cryptographic verification of DKIM-Signature headers in the message
  2. Authentication-Results header: If direct verification fails but the message contains an Authentication-Results header showing dkim=pass (see §6.4)
  3. ARC chain: If no direct DKIM signatures but a valid ARC chain exists with dkim=pass at the origin (i=1)

When the result comes from Authentication-Results, metadata.raw.evidence_refs SHOULD include "Authentication-Results". When the result comes from ARC, metadata.raw.evidence_refs SHOULD include "ARC-Authentication-Results".

from_domain_match

Indicates whether the DKIM signing domain (d=) matches or is a parent of the From header domain. This is a DKIM-level observation independent of DMARC evaluation.

body_length.limited

Indicates whether the DKIM signature uses the l= tag to limit body signing.

key_size

RSA key size in bits, when determinable from DNS lookup. Implementations SHOULD generate DKIM_WEAK_KEY_SIZE finding for keys smaller than 2048 bits.

6.3 Guidance

Weak DKIM conditions SHOULD be expressed via findings (e.g., relaxed body canonicalization, partial body signing, sha1).

When body_length.limited is true, the value field contains the byte count from the l= tag.

6.4 Authentication-Results Fallback (RFC 8601)

When analyzing forwarded emails (e.g., emails forwarded as attachments), DKIM signatures may fail re-verification due to transit modifications:

The original receiving server verified DKIM successfully at the time of receipt and recorded this in the Authentication-Results header (RFC 8601).

Implementations SHOULD trust Authentication-Results when ALL of the following conditions are met:

  1. The message contains DKIM-Signature headers (signatures were present)
  2. Direct cryptographic verification fails or returns neutral
  3. An Authentication-Results header exists with dkim=pass
  4. The passing DKIM domain in Authentication-Results matches a signature’s d= domain

When Authentication-Results is used: - A finding with ID DKIM_VIA_AUTH_RESULTS (severity INFO) SHOULD be added - metadata.raw.evidence_refs SHOULD include "Authentication-Results"

Security note: The Authentication-Results header represents a trusted assertion from the original receiving mail server. This is comparable to trusting ARC chains.

6.5 DKIM Archive Fallback (OPTIONAL)

When DKIM verification fails because the signing key is no longer published in DNS (key rotation, domain policy changes), implementations MAY query the DKIM Archive maintained by the ZK Email project for historical keys.

The DKIM Archive is a public database of historical DKIM public keys with over 1.4 million archived keys. Each archived key includes timestamps indicating when the key was first and last observed in DNS.

6.5.1 Archive API

The archive provides a REST API:

GET https://archive.prove.email/api/key/domain?domain={domain}&selector={selector}

Response: Array of archived keys with value, domain, selector, firstSeenAt, lastSeenAt fields.

Implementations MAY also contribute newly-verified domain/selector pairs:

POST https://archive.prove.email/api/dsp
Body: { "domain": "example.com", "selector": "s1" }

6.5.2 Archive Fallback Conditions

Implementations SHOULD query the archive when ALL of the following conditions are met:

  1. The message contains DKIM-Signature headers
  2. Direct cryptographic verification fails
  3. The failure is NOT due to body hash mismatch (message modification)
  4. The failure appears to be key-related (DNS lookup failure, key not found, key mismatch)

Implementations SHOULD NOT query the archive when:

6.5.3 Archive Verification

When an archived key is found:

  1. Re-attempt DKIM verification using the archived key
  2. If verification passes, treat as DKIM pass with reduced trust

6.5.4 Trust Model for Archive-Verified DKIM

Archive-verified DKIM provides weaker assurance than DNS-verified DKIM because:

Implementations MUST apply score penalties to reflect this reduced trust:

Key Age Severity Typical Penalty Rationale
< 1 year LOW -2 points Recent key rotation; high confidence
≥ 1 year MEDIUM -6 points Stale key; reduced confidence

Key age is determined by the lastSeenAt timestamp from the archive.

6.5.5 Findings

When archive verification is used:

Finding ID Severity When Generated
DKIM_VERIFIED_VIA_ARCHIVE LOW Archived key < 1 year old
DKIM_VERIFIED_VIA_STALE_ARCHIVE MEDIUM Archived key ≥ 1 year old

6.5.6 Metadata

When archive verification is used:

6.5.7 Contribution

Implementations are ENCOURAGED to contribute successfully DNS-verified domain/selector pairs to the archive. This creates a virtuous cycle: more contributions improve archive coverage for future verifications.

Contribution SHOULD be fire-and-forget (non-blocking) and SHOULD NOT affect response latency.

6.5.8 Privacy Considerations

The archive API receives only:

The archive does NOT receive email content, message headers, recipient addresses, or other message metadata. This exposure is equivalent to standard DNS queries.


7. SPF

7.1 SPFObject Schema

{
  "result": "PASS" | "FAIL" | "SOFTFAIL" | "NEUTRAL" | "UNVERIFIABLE" | "TEMPERROR" | "PERMERROR",
  "domain": "string | null",
  "mail_from": "string | null",
  "helo": "string | null",
  "ip": "string | null",
  "explanation": "string | null",
  "dns_lookups": 0,
  "verification_source": "DIRECT" | "ARC" | "NONE"
}

7.2 Static .eml Limitations and Trust Model

CRITICAL: SPF cannot be verified directly from static .eml files because the SMTP envelope sender (MAIL FROM) and connecting IP address are not preserved.

The Authentication-Results header is NOT a trusted source for SPF verification. Unlike DKIM signatures and ARC chains, Authentication-Results headers are trivially forgeable—any mail server can add this header with arbitrary claims. Trusting this header would allow attackers to forge SPF pass results.

SPF Trust Model: - SPF can ONLY be trusted via a valid ARC chain (cryptographically attested) - If no valid ARC chain exists, result MUST be "UNVERIFIABLE" - Authentication-Results header claims about SPF MUST NOT be trusted

In the UNVERIFIABLE case: - result MUST be "UNVERIFIABLE" - verification_source MUST be "NONE" - A finding with ID SPF_NOT_VERIFIABLE (severity INFO, 0 points) MUST be added - SPF MUST NOT count for or against verdict determination - SPF MUST NOT generate penalty findings (FAIL, SOFTFAIL, NEUTRAL)

7.3 SPF from Valid ARC Chain

If and only if a valid ARC chain exists (see §9) and the ARC-Authentication-Results at instance i=1 contains SPF results, those results MAY be trusted.

When SPF is derived from ARC: - verification_source MUST be "ARC" - The explanation field SHOULD note "From ARC chain" - SPF MAY contribute to verdict determination - SPF findings (FAIL, SOFTFAIL, NEUTRAL) MAY be generated with point penalties

7.4 SPF in LIVE Mode

When metadata.analysis.mode is LIVE, implementations have access to envelope data: - verification_source MUST be "DIRECT" - SPF verification proceeds normally per RFC 7208 - All SPF results (PASS, FAIL, SOFTFAIL, etc.) are authoritative

7.5 SPF Findings

ID Severity Points Condition
SPF_FAIL HIGH -12 SPF returned fail (verified sources only)
SPF_SOFTFAIL MEDIUM -6 SPF returned softfail (verified sources only)
SPF_NEUTRAL LOW -2 SPF returned neutral (verified sources only)
SPF_NOT_VERIFIABLE INFO 0 Static .eml lacks envelope context for SPF

7.6 Guidance

DNS lookup count may matter for risk and operational debugging.

SPF neutrality or softfail SHOULD appear as findings when relevant to policy, but ONLY when SPF is verifiable via ARC or direct verification.


8. DMARC

8.1 DMARCObject Schema

{
  "result": "PASS" | "FAIL" | "NONE" | "TEMPERROR" | "PERMERROR",
  "policy": "none" | "quarantine" | "reject" | "unknown",
  "pct": 100,
  "alignment": {
    "dkim": true,
    "spf": true,
    "mode": "relaxed" | "strict" | "unknown"
  },
  "domain": "string | null",
  "subdomain_policy": "none" | "quarantine" | "reject" | "unknown | null",
  "rua": ["string"],
  "ruf": ["string"],
  "explanation": "string | null"
}

8.2 DMARC Result Derivation

The effective DMARC result may come from multiple sources (in priority order):

  1. Authentication-Results header: If message contains Authentication-Results with dmarc= result (see §6.4 for trust model)
  2. ARC chain: If valid ARC chain exists with dmarc=pass at origin (i=1)
  3. Direct DMARC check: Via DNS lookup of _dmarc.<from-domain>
  4. Implicit pass: If DKIM alignment passes (signing domain matches From domain) and DKIM passes, DMARC is considered to pass even without explicit DMARC record

When the result comes from Authentication-Results, the explanation field SHOULD note "Determined from Authentication-Results header".

8.3 Alignment for Static .eml

For static .eml files: - alignment.dkim: Can be determined (signing domain vs From domain) - alignment.spf: SHOULD be false or null (cannot verify SPF alignment without envelope)

8.4 DMARC Policy Findings

ID Severity Points Condition
DMARC_POLICY_NONE MEDIUM -6 DMARC policy is p=none (no enforcement)
DMARC_POLICY_PARTIAL MEDIUM -6 DMARC pct < 100 (partial enforcement)
DMARC_FAIL HIGH -12 DMARC evaluation failed

Rationale for DMARC_POLICY_NONE at MEDIUM: A domain publishing p=none explicitly instructs receivers not to enforce authentication failures. This is a deliberate policy choice that enables spoofing attacks against the domain. While commonly used during DMARC deployment testing, it provides no protection in production.

Rationale for DMARC_POLICY_PARTIAL: When pct < 100, only a percentage of failing messages are subject to policy. An attacker can simply send more emails to ensure some bypass enforcement.


9. ARC (Authenticated Received Chain)

9.1 Overview

ARC preserves authentication results across mail forwarding. When an email is forwarded, the original DKIM signature may break, but ARC headers record authentication at each hop.

EBI uses ARC to recover original authentication results when direct verification is not possible.

9.2 ARCObject Schema

{
  "result": "PASS" | "FAIL" | "NONE" | "TEMPERROR" | "PERMERROR",
  "chain_valid": true,
  "instances": [
    {
      "i": 1,
      "cv": "pass" | "fail" | "none" | "unknown",
      "auth_results": "string | null",
      "signing_domain": "string | null"
    }
  ]
}

9.3 Field Semantics

chain_valid

Indicates whether the ARC chain is trustworthy:

Authentication results from ARC MUST NOT be trusted if chain_valid is false.

instances

Array of ARC set instances, sorted by i value (ascending). Each instance represents a hop in the forwarding chain.

9.4 Trusting ARC Results

When chain_valid is true, the authentication results from instance i=1 (the origin) MAY be used as effective results:

IF arc.chain_valid = true AND arc.instances[0].i = 1:
    effectiveDkim = parseResult(arc.instances[0].auth_results, "dkim")
    effectiveSpf = parseResult(arc.instances[0].auth_results, "spf")
    effectiveDmarc = parseResult(arc.instances[0].auth_results, "dmarc")

9.5 ARC Parsing

ARC headers are parsed from:

  1. ARC-Seal: Contains i=, cv=, d= (signing domain)
  2. ARC-Authentication-Results: Contains original auth results at each hop

Example ARC-Authentication-Results:

ARC-Authentication-Results: i=1; mx.google.com;
       dkim=pass header.d=example.com;
       spf=pass smtp.mailfrom=example.com;
       dmarc=pass header.from=example.com

10. Header Analysis

10.1 Overview

Authentication mechanisms verify domain ownership but not sender intent. An email can have perfect DKIM/SPF/DMARC and still be deceptive through:

The header_analysis object captures these indicators.

10.2 HeaderAnalysisObject Schema

{
  "display_name_analysis": {
    "from_display_name": "string | null",
    "from_email": "string",
    "from_domain": "string",
    "impersonation_indicators": [
      {
        "type": "BRAND_NAME" | "EXECUTIVE_TITLE" | "DOMAIN_IN_NAME" | "UNICODE_CONFUSABLE" | "OTHER",
        "matched": "string",
        "details": "string | null"
      }
    ]
  },
  "reply_to_analysis": {
    "reply_to_present": true,
    "reply_to_email": "string | null",
    "reply_to_domain": "string | null",
    "domain_mismatch": true,
    "signed": true
  },
  "header_anomalies": [
    {
      "header": "string",
      "issue": "string",
      "severity": "HIGH" | "MEDIUM" | "LOW"
    }
  ]
}

10.3 Display Name Impersonation

Implementations SHOULD check for:

Type Description Example
BRAND_NAME Known brand/company name in display name "Microsoft Support" <random@attacker.com>
EXECUTIVE_TITLE Executive titles suggesting authority "CEO" <phish@example.com>
DOMAIN_IN_NAME Email domain embedded in display name "support@company.com" <fake@attacker.com>
UNICODE_CONFUSABLE Unicode characters resembling ASCII "Microsοft" (Greek omicron)

10.4 Header Analysis Findings

ID Severity Points Condition
DISPLAY_NAME_BRAND_IMPERSONATION HIGH -12 Known brand name in display name from unrelated domain
DISPLAY_NAME_DOMAIN_MISMATCH MEDIUM -6 Display name contains email address different from actual From
REPLY_TO_DOMAIN_MISMATCH MEDIUM -6 Reply-To domain differs from From domain
UNICODE_CONFUSABLE_CHARS HIGH -12 Unicode characters that visually resemble ASCII in headers

11. Domain Intelligence

11.1 Overview

Domain threat intelligence provides additional context about the reputation and security posture of domains involved in an email. This analysis queries external threat intelligence APIs to assess:

Domain intelligence is optional. When the THREATS_API_KEY environment variable is not configured, domain_details will be null.

11.2 DomainDetailsObject Schema

{
  "from_domain": { /* DomainAnalysis | null */ },
  "to_domains": [ /* DomainAnalysis[] */ ],
  "lookup_errors": ["string"]
}

11.3 DomainAnalysis Schema

{
  "domain": "string",
  "reputation": {
    "score": 85,
    "risk_level": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
    "warnings": [
      {
        "code": 4001,
        "category": "string",
        "description": "string"
      }
    ],
    "dns_issues": ["string"],
    "mail_issues": ["string"],
    "ssl_issues": ["string"],
    "blocklist_flags": ["string"]
  },
  "malware": {
    "safe_score": 100,
    "is_dangerous": false,
    "threats": ["string"]
  },
  "infrastructure": {
    "web_servers": [{ "hostname": "string", "ip": "string", "country": "string | null", "region": "string | null", "network_name": "string | null" }],
    "mail_servers": [{ /* same as web_servers */ }],
    "name_servers": [{ /* same as web_servers */ }]
  },
  "whois": {
    "registration_date": "2010-03-15T00:00:00Z" | null,
    "expiration_date": "2025-03-15T00:00:00Z" | null,
    "domain_age_days": 5000 | null,
    "registrar": "GoDaddy" | null
  }
}

All fields in DomainAnalysis except domain may be null if the respective data source is unavailable.

11.4 WHOIS Data

Domain registration data is fetched via RDAP (Registration Data Access Protocol), the modern replacement for WHOIS. RDAP uses standard HTTPS on port 443, making it compatible with serverless environments that block raw TCP connections. Results are cached aggressively since domain registration dates don’t change. The cache persists to disk at the path specified by WHOIS_CACHE_FILE (default: /tmp/ebi-whois-cache.json).

Field Type Description
registration_date string | null ISO 8601 date when domain was registered
expiration_date string | null ISO 8601 date when domain registration expires
domain_age_days number | null Days since registration (calculated)
registrar string | null Domain registrar name

Domain age is a critical phishing signal. Domains registered within the past 7 days are flagged as high-risk in the formatted output.

11.5 Risk Level Thresholds

Risk Level Reputation Score
CRITICAL < 20
HIGH 20–39
MEDIUM 40–59
LOW ≥ 60

11.6 Warning Code Categories

Range Category
1000–1026 DNS/nameserver issues
2001–2009 WHOIS/registration issues
3001–3008 Website security issues
4001–4005 Blocklist presence
5000–5020 Mail server configuration
6001–6022 SSL/TLS issues

11.7 Domain Intelligence Findings

Only findings for the from_domain affect the EBI score. To-domain analysis is informational.

ID Severity Points Condition
DOMAIN_MALWARE_DETECTED CRITICAL -25 malware.safe_score < 50 OR malware.threats.length > 0
DOMAIN_BLOCKLISTED HIGH -12 Warning codes 4001–4005 present
DOMAIN_POOR_REPUTATION HIGH -12 reputation.score < 40
DOMAIN_RECENTLY_REGISTERED MEDIUM -6 Warning code 2001 present
DOMAIN_EXPIRED_REGISTRATION MEDIUM -6 Warning code 2002 present
DOMAIN_SSL_ISSUES MEDIUM -6 Warning codes 6001–6022 present
DOMAIN_NO_SPF LOW -2 Warning code 5015 present
DOMAIN_NO_DMARC LOW -2 Warning code 5016 present
DOMAIN_OFFSHORE INFO 0 Warning code 2008 present

12. Metadata

11.1 MetadataObject Schema

{
  "source": {
    "system": "string",
    "version": "string | null"
  },
  "analysis": {
    "mode": "LIVE" | "FORENSIC",
    "elapsed_ms": 0
  },
  "raw": {
    "header_hash": "string | null",
    "body_hash": "string | null",
    "evidence_refs": ["string"]
  }
}

11.2 Guidance

header_hash and body_hash allow correlation without storing raw content.

evidence_refs MAY contain internal URIs to stored raw artifacts.

When authentication results come from ARC, evidence_refs SHOULD include "ARC-Authentication-Results".

11.3 Analysis Mode

Mode Description
LIVE Real-time MTA integration with full envelope access
FORENSIC Static .eml analysis, limited to headers and ARC

The previous BATCH and TEST modes are deprecated. Implementations SHOULD map: - BATCHFORENSIC - TESTFORENSIC


13. Implementation Requirements

13.1 Determinism

Given identical inputs, an implementation MUST produce identical EBI outputs (except for timestamp and request_id).

13.2 Evidence-first

Every non-trivial verdict decision SHOULD be supported by either:

13.3 ARC Chain Validation

Implementations MUST:

13.4 Backward Compatibility

Clients MUST treat unknown fields as ignorable.

Fields MAY be added in future versions.

13.5 Score Method Versioning

When score.method changes between spec versions, implementations SHOULD:


14. Example Response

This example demonstrates a forwarded email where original DKIM verification happens via ARC chain.

{
  "ebi_version": "0.8",
  "request_id": "req_a1b2c3d4",
  "timestamp": "2026-01-18T10:30:00Z",
  "message_id": "<forwarded-msg@example.com>",

  "subject": "Important Document",
  "from": "sender@example.com",
  "from_display_name": "Sender Name",
  "to": ["recipient@company.com"],

  "request_context": {
    "forwarder_email": "analyst@company.com",
    "received_at": "2026-01-18T10:29:45Z"
  },

  "verdict": {
    "status": "AUTHENTIC",
    "confidence": "HIGH",
    "code": "ALL_PASS",
    "summary": "All authentication mechanisms passed.",
    "explanation": "Email appears genuine.",
    "flags": []
  },

  "score": {
    "value": 83,
    "scale": {"min": 0, "max": 100},
    "band": "GOOD",
    "method": "EBI_SCORE_V2",
    "components": {
      "base": 90,
      "finding_penalty": -12,
      "confidence_adjustment": 5
    },
    "notes": "AUTHENTIC + 2 findings"
  },

  "dkim": {
    "result": "PASS",
    "from_domain_match": true,
    "domain": "example.com",
    "selector": null,
    "signatures": []
  },

  "spf": {
    "result": "PASS",
    "domain": "example.com",
    "mail_from": null,
    "helo": null,
    "ip": null,
    "explanation": "From ARC chain",
    "dns_lookups": 3,
    "verification_source": "ARC"
  },

  "dmarc": {
    "result": "PASS",
    "policy": "quarantine",
    "pct": 100,
    "alignment": {"dkim": true, "spf": false, "mode": "relaxed"},
    "domain": "example.com",
    "subdomain_policy": null,
    "rua": [],
    "ruf": [],
    "explanation": "Determined from ARC authentication chain"
  },

  "arc": {
    "result": "PASS",
    "chain_valid": true,
    "instances": [
      {
        "i": 1,
        "cv": "none",
        "auth_results": "dkim=pass spf=pass dmarc=pass",
        "signing_domain": "forwarder.com"
      },
      {
        "i": 2,
        "cv": "pass",
        "auth_results": "dkim=none spf=none dmarc=none",
        "signing_domain": "mx.company.com"
      }
    ]
  },

  "header_analysis": {
    "display_name_analysis": {
      "from_display_name": "Sender Name",
      "from_email": "sender@example.com",
      "from_domain": "example.com",
      "impersonation_indicators": []
    },
    "reply_to_analysis": {
      "reply_to_present": false,
      "reply_to_email": null,
      "reply_to_domain": null,
      "domain_mismatch": false,
      "signed": true
    },
    "header_anomalies": []
  },

  "findings": [
    {
      "id": "DKIM_VIA_ARC",
      "severity": "INFO",
      "points": 0,
      "title": "DKIM verified via ARC chain",
      "summary": "Original DKIM signature was verified by forwarder.com.",
      "details": "The email was forwarded and the original DKIM signature was replaced with an ARC chain.",
      "evidence": {"type": "HEADER", "key": "ARC-Authentication-Results", "value": "dkim=pass"},
      "recommendation": "ARC provides trustworthy authentication for forwarded emails."
    },
    {
      "id": "DMARC_POLICY_NONE",
      "severity": "MEDIUM",
      "points": -6,
      "title": "DMARC policy not enforced",
      "summary": "Domain publishes p=none, which does not enforce authentication failures.",
      "details": "An attacker could spoof this domain and receivers would not reject the message.",
      "evidence": {"type": "DNS", "key": "_dmarc.example.com", "value": "v=DMARC1; p=none"},
      "recommendation": "Upgrade to p=quarantine or p=reject after monitoring."
    },
    {
      "id": "DKIM_MISSING_DATE_HEADER",
      "severity": "LOW",
      "points": -2,
      "title": "Date header not signed",
      "summary": "The Date header can be modified without invalidating the DKIM signature.",
      "details": null,
      "evidence": {"type": "DERIVED", "key": "dkim.signed_headers", "value": "from:to:subject:message-id"},
      "recommendation": "Include Date in h= tag."
    }
  ],

  "metadata": {
    "source": {"system": "ebi-service", "version": "0.3.0"},
    "analysis": {"mode": "FORENSIC", "elapsed_ms": 156},
    "raw": {
      "header_hash": null,
      "body_hash": null,
      "evidence_refs": ["ARC-Authentication-Results"]
    }
  }
}

15. Finding ID Registry

This section is normative. Implementations SHOULD use these exact IDs.

15.1 DKIM Findings

ID Severity Points Condition
DKIM_FAIL HIGH -12 DKIM verification failed
DKIM_PARTIAL_BODY_SIGNED CRITICAL -25 DKIM l= tag present; body can be appended
DKIM_WEAK_HASH_ALGO HIGH -12 DKIM uses rsa-sha1
DKIM_WEAK_KEY_SIZE HIGH -12 DKIM key size < 2048 bits
DKIM_MISSING_FROM_HEADER HIGH -12 From header not in signed headers (h=)
DKIM_MISSING_SUBJECT_HEADER MEDIUM -6 Subject header not in signed headers
DKIM_MISSING_TO_HEADER MEDIUM -6 To header not in signed headers
DKIM_MISSING_DATE_HEADER LOW -2 Date header not in signed headers
DKIM_MISSING_MESSAGE_ID_HEADER LOW -2 Message-ID header not in signed headers
DKIM_MISSING_REPLY_TO_HEADER HIGH -12 Reply-To header present but not signed (redirect attack vector)
DKIM_THIRD_PARTY_SIGNATURE INFO 0 Signature domain differs from From domain
DKIM_NO_AUTHOR_DOMAIN_SIGNATURE INFO 0 DKIM passed but no signature from From domain (common with ESPs)
DKIM_SIGNATURE_EXPIRED HIGH -12 Signature x= timestamp has passed
DKIM_VIA_ARC INFO 0 DKIM result inferred from valid ARC chain
DKIM_VIA_AUTH_RESULTS INFO 0 DKIM result inferred from Authentication-Results header (RFC 8601)
DKIM_VERIFIED_VIA_ARCHIVE LOW -2 DKIM verified using archived key < 1 year old (§6.5)
DKIM_VERIFIED_VIA_STALE_ARCHIVE MEDIUM -6 DKIM verified using archived key ≥ 1 year old (§6.5)
SENDER_NO_DKIM_KEYS INFO 0 Sender domain has no DKIM keys published at common selectors

15.2 SPF Findings

ID Severity Points Condition
SPF_FAIL HIGH -12 SPF returned fail (verified sources only)
SPF_SOFTFAIL MEDIUM -6 SPF returned softfail (verified sources only)
SPF_NEUTRAL LOW -2 SPF returned neutral (verified sources only)
SPF_NOT_VERIFIABLE INFO 0 Static .eml lacks envelope context for SPF

15.3 DMARC Findings

ID Severity Points Condition
DMARC_POLICY_NONE MEDIUM -6 DMARC policy is p=none (no enforcement)
DMARC_POLICY_PARTIAL MEDIUM -6 DMARC pct < 100 (partial enforcement)
DMARC_FAIL HIGH -12 DMARC evaluation failed
DMARC_TEMPERROR INFO 0 DMARC DNS lookup failed or no record published

15.4 ARC Findings

ID Severity Points Condition
ARC_CHAIN_FAIL HIGH -12 ARC chain validation failed (cv=fail)

15.5 Header Analysis Findings

ID Severity Points Condition
DISPLAY_NAME_BRAND_IMPERSONATION HIGH -12 Known brand name in display name from unrelated domain
DISPLAY_NAME_DOMAIN_MISMATCH MEDIUM -6 Display name contains email address different from actual From
REPLY_TO_DOMAIN_MISMATCH MEDIUM -6 Reply-To domain differs from From domain
UNICODE_CONFUSABLE_CHARS HIGH -12 Unicode characters that visually resemble ASCII in headers

15.6 Domain Intelligence Findings

ID Severity Points Condition
DOMAIN_MALWARE_DETECTED CRITICAL -25 Domain flagged for malware or phishing
DOMAIN_BLOCKLISTED HIGH -12 Domain on security blocklists (codes 4001–4005)
DOMAIN_POOR_REPUTATION HIGH -12 Reputation score below 40
DOMAIN_RECENTLY_REGISTERED MEDIUM -6 Domain recently registered (code 2001)
DOMAIN_EXPIRED_REGISTRATION MEDIUM -6 Domain registration expired (code 2002)
DOMAIN_SSL_ISSUES MEDIUM -6 SSL/TLS configuration problems (codes 6001–6022)
DOMAIN_NO_SPF LOW -2 No SPF record published (code 5015)
DOMAIN_NO_DMARC LOW -2 No DMARC record published (code 5016)
DOMAIN_OFFSHORE INFO 0 Registered in offshore jurisdiction (code 2008)

16. Changelog

v0.11.0

NEW: Domain Age via RDAP (§11.4)

DomainAnalysis now includes a whois field with actual registration dates:

"whois": {
  "registration_date": "2010-03-15T00:00:00Z",
  "expiration_date": "2025-03-15T00:00:00Z",
  "domain_age_days": 5000,
  "registrar": "GoDaddy"
}

Registration data is fetched via RDAP (Registration Data Access Protocol), the modern HTTPS-based replacement for WHOIS. This works in serverless environments where raw WHOIS (port 43) is blocked. Results are cached to disk for 30 days.

Display improvements:

Removed:


v0.10.0

NEW: Domain Intelligence (§11)

This release adds optional domain threat intelligence analysis. When configured with a THREATS_API_KEY, EBI queries external APIs for:

New response field:

New findings (§15.6):

ID Severity Points
DOMAIN_MALWARE_DETECTED CRITICAL -25
DOMAIN_BLOCKLISTED HIGH -12
DOMAIN_POOR_REPUTATION HIGH -12
DOMAIN_RECENTLY_REGISTERED MEDIUM -6
DOMAIN_EXPIRED_REGISTRATION MEDIUM -6
DOMAIN_SSL_ISSUES MEDIUM -6
DOMAIN_NO_SPF LOW -2
DOMAIN_NO_DMARC LOW -2
DOMAIN_OFFSHORE INFO 0

Notes:


v0.9.0

BREAKING: Scoring method upgraded to EBI_SCORE_V3 (§4.4)

This release recalibrates scoring to match human intuition for business email assessment:

Base score changes:

Status V2 V3 Rationale
AUTHENTIC 90 90 Unchanged
PARTIAL 55 75 Third-party ESP signing is normal, not suspicious
INCONCLUSIVE 40 55 Unverifiable needs review, not auto-fail
FAILED 15 35 Failed auth is bad, findings add detail
UNSAFE 60 25 Integrity vulnerabilities (l= tag) are critical

New findings:

New optional feature: DKIM Archive Fallback (§6.5)

Implementations MAY query the ZK Email DKIM Archive when DKIM verification fails due to key rotation. The archive contains 1.4M+ historical DKIM keys. Archive-verified DKIM receives reduced trust scores: - Recent keys (< 1 year): LOW severity, -2 points - Stale keys (≥ 1 year): MEDIUM severity, -6 points

Finding severity changes:

Bug fixes:

Migration notes:

v0.8.2

v0.8.1

v0.8

v0.7

v0.6

v0.5

v0.4

v0.3

v0.2

v0.1

v0.0


© 2026 Kapwork