{
  "openapi": "3.0.3",
  "info": {
    "title": "CA Chain Check API",
    "version": "1.0",
    "description": "Checks the TLS certificate chain a server sends: whether it reaches a root in the trust store, its order, validity dates, extra certificates, pending distrust, revocation (OCSP and CRL) and hostname. The chain is collected without trusting it, then analyzed. See also the plain-language guide at [/help](help)."
  },
  "servers": [
    {
      "url": "/",
      "description": "This server"
    }
  ],
  "tags": [
    {
      "name": "check",
      "description": "Run a chain check"
    },
    {
      "name": "service",
      "description": "Service information"
    }
  ],
  "paths": {
    "/api/check": {
      "post": {
        "tags": [
          "check"
        ],
        "operationId": "check",
        "summary": "Check a server's certificate chain",
        "description": "Connects to every IP address the host resolves to (or to `connectIPs`), collects the chain each one sends, and analyzes it. A failed connection to one IP is reported as `error` on that IP's result, and an address this server's policy does not allow (a private address, for example) is reported with `tested: false` and `denyReason`. Neither is an HTTP error.\n\nChecks are rate-limited per client address. A host with many IP addresses uses more of the limit than a host with one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CheckRequest"
              },
              "examples": {
                "https": {
                  "summary": "HTTPS on port 443",
                  "value": {
                    "host": "www.example.edu",
                    "port": 443
                  }
                },
                "smtp-auto": {
                  "summary": "SMTP submission (TLS, then STARTTLS)",
                  "value": {
                    "host": "smtp.example.edu",
                    "port": 587
                  }
                },
                "smtp-forced": {
                  "summary": "STARTTLS on a non-standard port",
                  "value": {
                    "host": "smtp.example.edu",
                    "port": 2525,
                    "starttls": "smtp"
                  }
                },
                "connect-ips": {
                  "summary": "Test specific IPs (like curl --resolve)",
                  "value": {
                    "host": "www.example.edu",
                    "port": 443,
                    "connectIPs": [
                      "192.0.2.10"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check finished. Look at each `results[].overall`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResponse"
                }
              }
            }
          },
          "400": {
            "description": "The request was not valid: the body is not JSON, is over 4 KB or has an unknown field; the hostname, port, STARTTLS protocol or a connect IP is not valid; the hostname does not resolve; too many connect IPs; or `connectIPs` was sent but this client may not use it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Too many checks from this address. Wait for `Retry-After` seconds before trying again.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before trying again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The DNS lookup failed, for example because the resolver timed out. A hostname that does not exist is a 400.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "This server has no trust store data yet (for example, the first download from CCADB failed and there is no built-in copy), so it cannot tell whether a chain is trusted. Try again after `Retry-After` seconds. This does not use the rate limit.",
            "headers": {
              "Retry-After": {
                "description": "Seconds until the server tries the download again.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/config": {
      "get": {
        "tags": [
          "service"
        ],
        "operationId": "config",
        "summary": "Options for this client",
        "responses": {
          "200": {
            "description": "Options",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Config"
                }
              }
            }
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": [
          "service"
        ],
        "operationId": "healthz",
        "summary": "Liveness check",
        "responses": {
          "200": {
            "description": "The service is running.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "ok\n"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "CheckRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "host"
        ],
        "properties": {
          "host": {
            "type": "string",
            "description": "Hostname or IP address. A hostname is sent as SNI. A URL is accepted and reduced to its hostname (`https://www.example.edu/path` becomes `www.example.edu`); international names are converted to their ASCII form. Do not include a port; use `port`.",
            "example": "www.example.edu"
          },
          "port": {
            "type": "integer",
            "minimum": 0,
            "maximum": 65535,
            "description": "TCP port. Leave out (or send 0) for 443, or for the protocol's usual port when `starttls` names one."
          },
          "starttls": {
            "type": "string",
            "enum": [
              "smtp",
              "imap",
              "pop3",
              "ftp",
              "ldap",
              "postgres",
              "none"
            ],
            "description": "Leave out for automatic: on ports 21, 25, 110, 143, 389, 587 and 5432 the check tries TLS first, then STARTTLS for that port's protocol if the TLS handshake fails. A protocol name uses STARTTLS straight away on any port. `none` uses TLS only."
          },
          "connectIPs": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IP addresses to connect to instead of looking up the hostname, like `curl --resolve`. The hostname is still sent as SNI. This server may not allow it; `GET /api/config` says whether you can. Sending more addresses than this server allows is a 400."
          }
        }
      },
      "CheckResponse": {
        "type": "object",
        "properties": {
          "host": {
            "type": "string"
          },
          "port": {
            "type": "integer"
          },
          "starttls": {
            "type": "string",
            "description": "STARTTLS protocol the request asked for, if any."
          },
          "starttlsFallback": {
            "type": "string",
            "description": "STARTTLS protocol tried when TLS failed, on a common STARTTLS port."
          },
          "checkedAt": {
            "type": "string",
            "format": "date-time"
          },
          "resolution": {
            "$ref": "#/components/schemas/Resolution"
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IPResult"
            },
            "description": "One entry per IP address."
          },
          "consistency": {
            "$ref": "#/components/schemas/Consistency"
          },
          "trustStore": {
            "$ref": "#/components/schemas/TrustStoreInfo"
          }
        }
      },
      "Resolution": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "dns",
              "config_override",
              "request_override",
              "ip_literal"
            ],
            "description": "Where the IP addresses came from."
          },
          "ips": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IP addresses tested."
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IP addresses not tested because of this server's per-host limit."
          },
          "ipv4Only": {
            "type": "boolean",
            "description": "This server tests IPv4 only."
          },
          "droppedIPv6": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "IPv6 addresses found but not tested."
          }
        }
      },
      "IPResult": {
        "type": "object",
        "properties": {
          "ip": {
            "type": "string"
          },
          "tested": {
            "type": "boolean"
          },
          "denyReason": {
            "type": "string",
            "description": "Set when this server's policy does not allow testing this IP."
          },
          "error": {
            "type": "string",
            "description": "Set when the connection, STARTTLS or TLS handshake failed. There is no `analysis` then."
          },
          "tls": {
            "$ref": "#/components/schemas/TLSInfo"
          },
          "analysis": {
            "$ref": "#/components/schemas/Analysis"
          },
          "overall": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info"
            ],
            "description": "Worst status for this IP."
          },
          "keyType": {
            "type": "string",
            "enum": [
              "rsa",
              "ecdsa",
              "other"
            ],
            "description": "Key type of the leaf in `analysis`."
          },
          "alternate": {
            "$ref": "#/components/schemas/AltResult"
          },
          "keyTypeNote": {
            "type": "string",
            "description": "Why there is no `alternate`: the server has only one certificate, or it could not be asked (for example a TLS 1.3-only server).",
            "example": "The server has only an ECDSA certificate."
          }
        }
      },
      "TLSInfo": {
        "type": "object",
        "properties": {
          "version": {
            "type": "string",
            "example": "TLS 1.3"
          },
          "cipherSuite": {
            "type": "string",
            "example": "TLS_AES_128_GCM_SHA256"
          },
          "alpn": {
            "type": "string"
          },
          "sni": {
            "type": "string",
            "description": "Server name sent. Empty for IP literals."
          },
          "handshakeMs": {
            "type": "integer",
            "description": "TLS handshake time in milliseconds."
          },
          "starttls": {
            "type": "string",
            "description": "STARTTLS protocol used before the handshake, if any."
          }
        }
      },
      "Analysis": {
        "type": "object",
        "properties": {
          "chain": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Certificate"
            },
            "description": "Certificates in the order the server sent them."
          },
          "storeRoots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Certificate"
            },
            "description": "Roots from the trust store that complete a path but were not sent."
          },
          "paths": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Path"
            }
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Check"
            }
          },
          "overall": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info"
            ]
          },
          "mermaid": {
            "type": "string",
            "description": "Mermaid flowchart source for the paths diagram."
          },
          "diagramKey": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/KeyEntry"
            },
            "description": "Styles the diagram uses, in display order."
          },
          "trustStores": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StoreResult"
            },
            "description": "One entry per root program, in the order Apple, Chrome, Microsoft, Mozilla."
          },
          "staple": {
            "$ref": "#/components/schemas/Staple"
          }
        }
      },
      "Check": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "mozilla_root",
              "order",
              "validity",
              "extra_certs",
              "pending_distrust",
              "revocation",
              "ocsp_stapling",
              "hostname",
              "root_sent",
              "chain"
            ],
            "description": "`mozilla_root`: a path ends at a root in the trust store. `chain`: only when the server sent no certificates. `revocation`: no certificate on the best path is revoked, by OCSP or CRL."
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info"
            ]
          },
          "detail": {
            "type": "string"
          },
          "items": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Certificates or values the result is about, as `<certificate>: <problem>` where that applies."
          }
        }
      },
      "Path": {
        "type": "object",
        "properties": {
          "nodes": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Certificate `id`s, leaf first."
          },
          "terminal": {
            "type": "string",
            "enum": [
              "mozilla_root",
              "untrusted_root",
              "incomplete"
            ],
            "description": "`mozilla_root`: ends at a root in the trust store."
          },
          "missingIssuer": {
            "type": "string",
            "description": "For incomplete paths: the issuer that could not be found."
          },
          "trusted": {
            "type": "boolean",
            "description": "The path ends at a root that at least one root program fully trusts."
          },
          "timeValid": {
            "type": "boolean",
            "description": "Every certificate on the path is within its dates."
          },
          "pendingDistrust": {
            "type": "boolean"
          },
          "best": {
            "type": "boolean"
          },
          "stores": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "apple",
                "chrome",
                "microsoft",
                "mozilla"
              ]
            },
            "description": "Root programs that fully trust the root this path ends at."
          },
          "limitedStores": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "apple",
                "chrome",
                "microsoft",
                "mozilla"
              ]
            },
            "description": "Root programs that trust the end root only for certificates issued before a cutoff date."
          }
        }
      },
      "Certificate": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Used by `paths` and the diagram."
          },
          "index": {
            "type": "integer",
            "description": "Position as sent; -1 if not sent."
          },
          "source": {
            "type": "string",
            "enum": [
              "presented",
              "mozilla_store"
            ],
            "description": "`mozilla_store`: from the trust store, not sent."
          },
          "sha256": {
            "type": "string",
            "description": "Lowercase hex."
          },
          "sha1": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "issuer": {
            "type": "string"
          },
          "subjectCN": {
            "type": "string"
          },
          "issuerCN": {
            "type": "string"
          },
          "serial": {
            "type": "string"
          },
          "version": {
            "type": "integer"
          },
          "notBefore": {
            "type": "string",
            "format": "date-time"
          },
          "notAfter": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "string",
            "enum": [
              "valid",
              "expired",
              "not_yet_valid",
              "expiring_soon"
            ]
          },
          "daysRemaining": {
            "type": "integer"
          },
          "keyAlgorithm": {
            "type": "string"
          },
          "keySize": {
            "type": "integer"
          },
          "signatureAlgorithm": {
            "type": "string"
          },
          "dnsNames": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ipAddresses": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "uris": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "subjectKeyId": {
            "type": "string"
          },
          "authorityKeyId": {
            "type": "string"
          },
          "isCA": {
            "type": "boolean"
          },
          "maxPathLen": {
            "type": "integer",
            "nullable": true
          },
          "keyUsage": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "extKeyUsage": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "policies": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "ocspServers": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "issuingUrls": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "crlDistributionPoints": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "selfSigned": {
            "type": "boolean"
          },
          "inMozilla": {
            "type": "boolean",
            "description": "True when at least one root program (Apple, Chrome, Microsoft or Mozilla) fully trusts this root. The name is kept for compatibility."
          },
          "pendingDistrust": {
            "$ref": "#/components/schemas/PendingDistrust"
          },
          "onTrustedPath": {
            "type": "boolean"
          },
          "extraneous": {
            "type": "string",
            "enum": [
              "unrelated",
              "alternate_path",
              "duplicate"
            ],
            "description": "Set when the certificate is not needed."
          },
          "duplicateOf": {
            "type": "integer",
            "description": "Position of the first copy, for duplicates."
          },
          "pem": {
            "type": "string"
          },
          "revocation": {
            "$ref": "#/components/schemas/Revocation"
          },
          "stores": {
            "type": "object",
            "description": "Root programs that include this root, mapped to `trusted` or `limited` (Microsoft roots trusted only for certificates issued before a cutoff date). Only set for roots in the trust store data.",
            "additionalProperties": {
              "type": "string",
              "enum": [
                "trusted",
                "limited"
              ]
            },
            "example": {
              "apple": "trusted",
              "chrome": "trusted",
              "microsoft": "trusted",
              "mozilla": "trusted"
            }
          }
        }
      },
      "PendingDistrust": {
        "type": "object",
        "properties": {
          "fingerprint": {
            "type": "string",
            "description": "SHA-256, lowercase hex."
          },
          "date": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "KeyEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "node",
              "link"
            ]
          },
          "fill": {
            "type": "string"
          },
          "stroke": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "width": {
            "type": "number"
          },
          "dash": {
            "type": "string",
            "description": "SVG stroke-dasharray; empty is solid."
          },
          "hatch": {
            "type": "boolean"
          },
          "class": {
            "type": "string",
            "description": "Mermaid class the node style uses."
          }
        }
      },
      "Consistency": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "info"
            ]
          },
          "detail": {
            "type": "string"
          },
          "groups": {
            "type": "array",
            "items": {
              "type": "array",
              "items": {
                "type": "string"
              }
            },
            "description": "IP addresses grouped by identical chains."
          }
        }
      },
      "TrustStoreInfo": {
        "type": "object",
        "properties": {
          "source": {
            "type": "string",
            "description": "`CCADB` when the data was downloaded by this server, or `embedded snapshot` when it is the copy built into the server."
          },
          "count": {
            "type": "integer",
            "description": "Roots loaded. 0 means the server has no trust store data and `POST /api/check` answers 503."
          },
          "loadedAt": {
            "type": "string",
            "format": "date-time",
            "description": "When the data was downloaded from CCADB. For the built-in copy, the date it was downloaded for the build."
          },
          "stores": {
            "type": "object",
            "description": "Number of roots each program fully trusts for server authentication.",
            "additionalProperties": {
              "type": "integer"
            },
            "example": {
              "apple": 116,
              "chrome": 100,
              "microsoft": 204,
              "mozilla": 118
            }
          },
          "refreshFailing": {
            "type": "boolean",
            "description": "The last download from CCADB failed. The server keeps the data it has and retries, waiting longer after each failure (1 minute, doubling, at most 1 hour)."
          },
          "nextAttempt": {
            "type": "string",
            "format": "date-time",
            "description": "When the server tries the download again. Only set while `refreshFailing` is true."
          }
        }
      },
      "Config": {
        "type": "object",
        "properties": {
          "ipOverrideAllowed": {
            "type": "boolean",
            "description": "Whether this client may send `connectIPs`."
          },
          "trustStore": {
            "allOf": [
              {
                "$ref": "#/components/schemas/TrustStoreInfo"
              }
            ],
            "description": "The root store data this server checks against."
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "Revocation": {
        "type": "object",
        "description": "What the issuer's OCSP responder and CRL say. Only set for certificates on the best path whose issuer is known, when the server checks revocation. A source is missing when the certificate names no http(s) URL for it.",
        "properties": {
          "ocsp": {
            "$ref": "#/components/schemas/RevocationSource"
          },
          "crl": {
            "$ref": "#/components/schemas/RevocationSource"
          }
        }
      },
      "RevocationSource": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "good",
              "revoked",
              "unknown",
              "error"
            ],
            "description": "`unknown`: the OCSP responder does not know the certificate. `error`: it could not be checked; see `error`."
          },
          "error": {
            "type": "string"
          },
          "revokedAt": {
            "type": "string",
            "format": "date-time"
          },
          "reason": {
            "type": "string",
            "example": "key compromise"
          },
          "thisUpdate": {
            "type": "string",
            "format": "date-time"
          },
          "nextUpdate": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "StoreResult": {
        "type": "object",
        "description": "Whether any path is trusted by one root program.",
        "required": [
          "store",
          "label",
          "status",
          "required"
        ],
        "properties": {
          "store": {
            "type": "string",
            "enum": [
              "apple",
              "chrome",
              "microsoft",
              "mozilla"
            ]
          },
          "label": {
            "type": "string",
            "example": "Mozilla"
          },
          "status": {
            "type": "string",
            "enum": [
              "trusted",
              "limited",
              "untrusted"
            ]
          },
          "required": {
            "type": "boolean",
            "description": "Whether this program counts toward the `mozilla_root` check. Set by the server's REQUIRED_TRUST_STORES."
          }
        }
      },
      "AltResult": {
        "type": "object",
        "description": "The second chain, from a server that has both an RSA and an ECDSA certificate. Found with a TLS 1.2 handshake offering only the other key type's cipher suites. `overall` on the IP result is the worse of the two.",
        "required": [
          "keyType",
          "tls",
          "analysis",
          "overall"
        ],
        "properties": {
          "keyType": {
            "type": "string",
            "enum": [
              "rsa",
              "ecdsa"
            ]
          },
          "tls": {
            "$ref": "#/components/schemas/TLSInfo"
          },
          "analysis": {
            "$ref": "#/components/schemas/Analysis"
          },
          "overall": {
            "type": "string",
            "enum": [
              "pass",
              "warn",
              "fail",
              "info"
            ]
          }
        }
      },
      "Staple": {
        "type": "object",
        "description": "The OCSP response the server stapled to the handshake, checked against the leaf and its issuer on the best path.",
        "required": [
          "present",
          "mustStaple"
        ],
        "properties": {
          "present": {
            "type": "boolean",
            "description": "The server stapled a response."
          },
          "mustStaple": {
            "type": "boolean",
            "description": "The leaf has the TLS Feature extension with status_request (Must-Staple)."
          },
          "status": {
            "type": "string",
            "enum": [
              "good",
              "revoked",
              "unknown",
              "error"
            ],
            "description": "`error`: the staple can't be used (bad signature, wrong certificate, expired, or the issuer is unknown); see `error`."
          },
          "thisUpdate": {
            "type": "string",
            "format": "date-time"
          },
          "nextUpdate": {
            "type": "string",
            "format": "date-time"
          },
          "revokedAt": {
            "type": "string",
            "format": "date-time"
          },
          "error": {
            "type": "string"
          }
        }
      }
    }
  }
}
