Website Legitimacy Index

Website Legitimacy Index (WLI) Specification — v0.3.0

Status: Draft

Last Updated: 2026-02-02


Overview

WLI provides a structured, machine-readable assessment of a domain’s TLS posture and operational legitimacy. It quantifies trust signals, flags weaknesses, and produces an evidence trail for downstream verification decisions.

TLS certificates are powerful but their surface details hide important nuance. A valid certificate from a recognized CA tells you nothing about whether the operator invested in organizational validation, maintains their infrastructure, enforces transport security, or participates in Certificate Transparency. WLI surfaces this information as discrete, scored signals that compose into a trust grade.

WLI is the domain layer in Kapwork’s verification stack. Invoice-level and record-level certificates inherit WLI attestations.


Prerequisite Reading

Topic What It Does Specification
TLS 1.3 Transport encryption with forward secrecy RFC 8446
X.509 Certificate format for public key infrastructure RFC 5280
Certificate Transparency Public audit logs for issued certificates RFC 6962
OCSP Online certificate revocation checking RFC 6960
HSTS HTTP Strict Transport Security RFC 6797
W3C Verifiable Credentials Standard for cryptographic attestations W3C VC Data Model v2.0

Design Goals

WLI is:


1. Technical Specification

1.1 What is WLI?

WLI is a structured JSON response (W3C Verifiable Credential) summarizing domain trust by analyzing:

WLI surfaces operational quality indicators that distinguish well-maintained domains from neglected or fraudulent ones. Domain threat intelligence is fetched from ELI’s threat intel service when available.

1.2 Analogy

WLI is to domain trust what a credit report is to financial trustworthiness: a structured snapshot of observable signals that inform but do not determine a decision.

1.3 Non-goals


2. Response Schema

2.1 Top-level Object

A WLI response MUST be a W3C Verifiable Credential with the following structure.

{
  "@context": [
    "https://www.w3.org/ns/credentials/v2",
    {
      "kw": "https://schema.kapwork.com/credentials/v1#",
      "kw:domainAttestation": { "@id": "kw:domainAttestation", "@type": "@json" },
      "kw:tlsPosture": { "@id": "kw:tlsPosture", "@type": "@json" },
      "kw:trustScore": { "@id": "kw:trustScore", "@type": "@json" }
    }
  ],
  "id": "urn:uuid:<uuid>",
  "type": ["VerifiableCredential", "KapworkDomainAttestation"],
  "issuer": {
    "id": "did:web:verify.kapwork.com",
    "name": "Kapwork, Inc.",
    "url": "https://kapwork.com"
  },
  "validFrom": "<RFC3339>",
  "validUntil": "<RFC3339>",
  "credentialSubject": { ... }
}

2.2 Credential Subject

The credentialSubject MUST contain:

Field Type Description
id string https://<host>
type string "Domain"
domain object Host, port, inspection timestamp
tlsPosture object Protocol, cipher, ALPN, chain trust status
certificate object Subject, issuer, serial, validity, key type, fingerprint, extensions
chain array Certificate chain with depth, role, subject, issuer, expiry
trustScore object Composite score, grade, summary, signal array
httpPosture object Status code, server header, HSTS, CSP, X-Frame-Options
domainThreatIntel object Domain threat intelligence: age, reputation, malware, blocklists

2.3 Optional Fields

Field Type Description
debtor object Debtor name and domain (set by invoice-level service)
invoiceReference string Invoice identifier (set by invoice-level service)

3. Trust Scoring

3.1 Scoring Method: WLI_SCORE_V1

The composite score is a weighted average of individual signal scores.

Each signal produces a score from 0 to 100 and carries a weight. The composite is:

composite = round(Σ(signal_score × signal_weight) / Σ(signal_weight))

3.2 Critical Cap

If any signal has severity critical, the composite is capped at 25 regardless of the weighted average. This prevents a domain with a broken trust chain or expired certificate from scoring well on other signals.

3.3 Signals

ID Weight Description
chain.trusted 3.0 Certificate chain validates against system trust store
cert.expiry 3.0 Days to certificate expiry
domain.reputation 2.0 Domain reputation score from threat intelligence
domain.malware 2.0 Domain malware check from threat intelligence
domain.blocklist 2.0 Domain blocklist flags from threat intelligence
domain.age 1.5 Domain registration age from WHOIS/RDAP
cert.issuer 1.0 Issuer tier (commercial / free / cloud / unknown)
protocol.version 1.0 TLS 1.3 / 1.2 / deprecated
chain.completeness 1.0 Chain depth, intermediate and root presence
cert.keyStrength 0.5 RSA key size or ECDSA
cert.signatureAlg 0.5 SHA-256+ vs SHA-1
cert.sanMatch 0.5 Hostname matches a Subject Alternative Name
cert.validation 0.5 DV / OV / EV validation type
cert.age 0.5 Days since certificate issuance
http.hsts 0.5 HSTS header presence and max-age
cert.transparency 0.5 SCT count from Certificate Transparency logs
cert.revocation 0.5 OCSP and/or CRL availability

Total weight: 20.5 with threat intel (17 signals), 13.0 without (TLS signals only)

When threat intelligence is unavailable, the four domain.* signals return score=0 with weight=0, effectively excluding them from the composite. This avoids penalizing a domain for WLI’s failure to fetch external data.

3.4 Signal Severities

Each signal reports a severity level:

Severity Meaning
pass Signal is healthy
info Acceptable but not optimal
warn Potential weakness
fail Problem detected
critical Serious problem, triggers score cap

3.5 Grades

Grade Score Range
A 90-100
B 80-89
C 65-79
D 50-64
F 0-49

3.6 Scoring Rationale

The weighted average model was chosen over the base+penalty model used by ELI because domain trust signals are independent observations, not deductions from a baseline. There is no inherent “starting score” for a domain. Each signal contributes proportionally to the composite.

Weights reflect the signal’s relevance to operational legitimacy in a receivables verification context:


4. Signal Specifications

4.1 chain.trusted

Checks whether the certificate chain validates against the system trust store.

Condition Score Severity
Chain validates 100 pass
Chain does not validate 0 critical

4.2 protocol.version

Condition Score Severity
TLS 1.3 100 pass
TLS 1.2 95 pass
TLS 1.1 or below 0 critical

4.3 cert.expiry

Condition Score Severity
Expired 0 critical
< 7 days 10 critical
7-13 days 30 warn
14-29 days 60 warn
30-59 days 80 info
60+ days 100 pass

4.4 cert.age

Certificate age is informational. Frequent rotation is a good security practice, so age alone does not penalize.

Condition Score Severity
Future issuance date 0 critical
Any valid age 100 pass

4.5 cert.validation

Condition Score Severity
Extended Validation (EV) 100 pass
Organization Validated (OV) 100 pass
Domain Validated (DV) 100 pass

4.6 cert.issuer

Condition Score Severity
Tier 1 commercial CA (DigiCert, Entrust, GlobalSign, Sectigo) 100 pass
Tier 2 (Let’s Encrypt, ZeroSSL, cloud CAs, GoDaddy) 100 pass
Known issuer, untiered 100 pass
Unknown issuer (no Organization name) 100 info

4.7 cert.keyStrength

Condition Score Severity
ECDSA or EdDSA 100 pass
RSA 4096+ 100 pass
RSA 2048 80 pass
RSA < 2048 0 critical

4.8 cert.signatureAlg

Condition Score Severity
SHA-256, SHA-384, SHA-512, EdDSA 100 pass
SHA-1 0 critical
Unknown/undetermined 50 info

Requires --extended flag. Without it, reports 50/info.

4.9 chain.completeness

Condition Score Severity
No chain received 0 critical
Leaf only 80 info
2+ certs including root 100 pass
2+ certs without root 100 pass

4.10 http.hsts

Condition Score Severity
Not set 80 info
Present, unparseable max-age 90 info
max-age < 1 day 85 info
max-age < 6 months 95 pass
max-age >= 6 months 100 pass

4.11 cert.transparency

Condition Score Severity
2+ SCTs 100 pass
1 SCT 90 pass
SCT extension present but unparseable 90 pass
No SCTs 70 info

Requires --extended flag. Without it, reports 80/info.

4.12 cert.revocation

Condition Score Severity
OCSP + CRL 100 pass
OCSP only 100 pass
CRL only 90 pass
Neither 70 info

Requires --extended flag. Without it, reports 80/info.

4.13 cert.sanMatch

Condition Score Severity
No SANs in certificate 30 warn
Hostname matches (exact or wildcard) 100 pass
Hostname does not match any SAN 10 critical

4.14 domain.age

Domain registration age from WHOIS/RDAP lookup. Very new domains are high-risk indicators.

When threat intel is unavailable, this signal uses weight=0 to exclude itself from the composite (see §3.3).

Condition Score Severity
Threat intel unavailable 0 info
< 30 days old 0 critical
30-89 days old 50 warn
90-364 days old 80 info
365+ days old 100 pass

4.15 domain.reputation

Domain reputation score from ThreatIntelligencePlatform.com API.

When threat intel is unavailable, this signal uses weight=0 to exclude itself from the composite (see §3.3).

Condition Score Severity
Threat intel unavailable 0 info
Reputation < 20 0 critical
Reputation 20-39 30 fail
Reputation 40-59 60 warn
Reputation 60+ 100 pass

4.16 domain.malware

Domain malware check from ThreatIntelligencePlatform.com API.

When threat intel is unavailable, this signal uses weight=0 to exclude itself from the composite (see §3.3).

Condition Score Severity
Threat intel unavailable 0 info
is_dangerous = true 0 critical
safe_score < 50 30 warn
safe_score 50+ 100 pass

4.17 domain.blocklist

Blocklist flags from domain reputation analysis.

When threat intel is unavailable, this signal uses weight=0 to exclude itself from the composite (see §3.3).

Condition Score Severity
Threat intel unavailable 0 info
Any blocklist flags present 0 critical
No blocklist flags 100 pass

5. Credential Signing

5.1 Overview

WLI signs attestation credentials as JWS (JSON Web Signature, RFC 7515) using compact serialization. The signing key is the Kapwork ASP key pair published via the ASPE protocol at Keyoxide.

The credential JSON is the JWS payload. The signature proves that Kapwork issued the attestation and that the content has not been modified.

5.2 Signing Key

The signing key is an Ed25519 or P-256 key pair. The same key pair is used to sign the Kapwork ASP profile at Keyoxide. This creates a verifiable link between the WLI attestation and the Kapwork identity.

ASPE URI: aspe:keyoxide.org:EYOOS2SHKRAAQTVJ2APXDSMXYU

ASPE endpoint: GET https://keyoxide.org/.well-known/aspe/id/EYOOS2SHKRAAQTVJ2APXDSMXYU

5.3 JWS Protected Header

Field Value Description
alg EdDSA or ES256 Signing algorithm (matches key type)
typ vc+jwt Media type per W3C VC-JOSE-COSE
jwk {...} Public key in JWK format (inline for convenience)
kid aspe:keyoxide.org:EYOOS2SHKRAAQTVJ2APXDSMXYU Key identifier (ASPE URI for discovery)

5.4 Verification Flow

To verify a signed WLI attestation:

  1. Decode the JWS protected header.
  2. Extract the kid field (ASPE URI).
  3. Fetch the ASP profile JWS from the Keyoxide ASPE endpoint.
  4. Decode the ASP profile JWS header to extract the authoritative public key.
  5. If the attestation JWS contains an inline jwk, confirm it matches the ASPE-sourced key.
  6. Verify the attestation JWS signature using the ASPE-sourced public key.
  7. Parse the JWS payload as the W3C Verifiable Credential JSON.

The verification endpoint (POST /verify) implements this flow. Alternatively, the public key is available at GET /.well-known/jwks.json for clients that prefer JWKS-based discovery.

5.5 Unsigned Credentials

If the WLI server has no signing key configured (WLI_SIGNING_KEY / WLI_SIGNING_KEY_FILE not set), credentials are issued without a JWS wrapper. The signed field in the API response indicates whether the credential was signed.

Unsigned credentials are structurally valid W3C VCs but carry no cryptographic proof of issuance. They are suitable for development, testing, and internal use but should not be trusted in production verification flows.


6. Output Formats

6.1 Machine-readable

W3C Verifiable Credential JSON-LD, as specified in §2.

6.2 Human-readable

Plain text report with sections:

KAPWORK DOMAIN ATTESTATION
Certificate ID: urn:uuid:...
Issued: <timestamp>
Valid until: <timestamp>
Issuer: Kapwork, Inc.

DEBTOR (optional)
  Name:   ...
  Domain: ...

DOMAIN
  Host:       ...
  Protocol:   ...
  Cipher:     ...
  Trusted:    Yes/NO

TRUST SCORE
  Grade X (nn/100). Summary.

FINDINGS
  [severity] finding text
  ...

CERTIFICATE
  Subject/Issuer/Serial/Validity/Key/SHA-256/etc.

CERTIFICATE CHAIN
  [depth] ROLE: subject
      Issued by: issuer
      Expires:   nn days

DISCLAIMERS
  Point-in-time attestation, no warranty on future state.

7. API Endpoints

7.1 POST /inspect

Raw TLS inspection. Returns connection, certificate, chain, and HTTP header data.

Request body:

{
  "url": "example.com",
  "extended": true,
  "pem": false,
  "score": false
}

7.2 POST /score

Trust score only. Runs extended inspection internally.

Request body:

{
  "url": "example.com"
}

Response:

{
  "host": "example.com",
  "inspectedAt": "<RFC3339>",
  "score": 89,
  "grade": "B",
  "summary": "Grade B (89/100). No issues detected.",
  "signals": [...]
}

7.3 POST /certificate

Kapwork Domain Attestation. Returns W3C Verifiable Credential.

Request body:

{
  "url": "example.com",
  "debtor": { "name": "Acme Corp", "domain": "acme.com" },
  "invoiceRef": "INV-2025-00142"
}

Response varies by Accept header: - application/json (default): W3C VC JSON - text/plain: human-readable report

7.4 POST /verify

Verify a signed WLI attestation. Fetches the public key from Keyoxide ASPE and verifies the JWS signature.

Request body (JSON):

{
  "jws": "eyJhbGciOiJFZERTQSIs..."
}

Or raw JWS with Content-Type: application/jwt.

Response (valid):

{
  "valid": true,
  "issuer": "did:web:verify.kapwork.com",
  "keySource": "inline+aspe",
  "credential": { ... }
}

Response (invalid):

{
  "valid": false,
  "error": "signature verification failed"
}

7.5 GET /.well-known/jwks.json

Returns the public signing key in JWKS format.

{
  "keys": [{
    "kty": "OKP",
    "crv": "Ed25519",
    "x": "...",
    "use": "sig",
    "alg": "EdDSA",
    "kid": "aspe:keyoxide.org:EYOOS2SHKRAAQTVJ2APXDSMXYU"
  }]
}

Returns 404 if no signing key is configured.

7.6 GET /health

{
  "status": "ok",
  "timestamp": "<RFC3339>"
}

All POST endpoints have GET equivalents with ?url= query parameter.

7.7 GET /scanner

Web-based UI for interactive domain scanning. Serves a single-page HTML application that lets users enter a domain, submits it to POST /score, and displays the grade, score, signals, and a link to the full W3C VC certificate.

No authentication required. The scanner is intended for demos and ad-hoc use.


8. Issuer Tiers

WLI groups certificate issuers into tiers as a signal of the operator’s investment in their TLS posture. This is not a judgment on CA quality. A free DV cert from Let’s Encrypt is cryptographically sound. But a paid OV cert from DigiCert signals that the operator went through organizational validation and paid for the privilege, which is a meaningful operational signal in a receivables context.

Tier 1: Commercial CAs with OV/EV validation

DigiCert, Entrust, GlobalSign, Sectigo.

Tier 2: Free/automated CAs and cloud vendor managed certs

Let’s Encrypt, ZeroSSL, Buypass, AWS ACM, Google Trust Services, Cloudflare, GoDaddy, Microsoft.


9. Relationship to ELI

WLI and ELI are siblings. ELI assesses email authentication (DKIM, SPF, DMARC). WLI assesses domain TLS posture. Both produce scored attestations with supporting evidence. They are designed to be composed:

The scoring models differ intentionally. ELI uses base+penalty because emails start from a verdict baseline. WLI uses weighted average because domain signals are independent observations with no inherent baseline.


10. Limitations


Changelog

v0.3.0

v0.2.0

v0.1.0


© 2026 Kapwork