From 0d35040a400c5b81e933c1f40125f97732bcb9b1 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Apr 2026 02:50:46 +0700 Subject: [PATCH] Add CAA checker, based on TLS observations --- checkers/caa.go | 34 +++ generate.go | 1 + go.mod | 1 + go.sum | 2 + tools/gen_caa_issuers.go | 405 ++++++++++++++++++++++++++ web/src/lib/services/caa-issuers.json | 146 ++++++++++ web/src/lib/services/caa-issuers.ts | 171 +---------- 7 files changed, 601 insertions(+), 159 deletions(-) create mode 100644 checkers/caa.go create mode 100644 tools/gen_caa_issuers.go create mode 100644 web/src/lib/services/caa-issuers.json diff --git a/checkers/caa.go b/checkers/caa.go new file mode 100644 index 00000000..335309f0 --- /dev/null +++ b/checkers/caa.go @@ -0,0 +1,34 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2020-2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package checkers + +import ( + caa "git.happydns.org/checker-caa/checker" + sdk "git.happydns.org/checker-sdk-go/checker" + "git.happydns.org/happyDomain/internal/checker" +) + +func init() { + prvd := caa.Provider() + checker.RegisterObservationProvider(prvd) + checker.RegisterExternalizableChecker(prvd.(sdk.CheckerDefinitionProvider).Definition()) +} diff --git a/generate.go b/generate.go index 0d1abe76..36ff29f4 100644 --- a/generate.go +++ b/generate.go @@ -27,5 +27,6 @@ package main //go:generate go run tools/gen_rr_typescript.go web/src/lib/dns_rr.ts //go:generate go run tools/gen_service_specs.go -o web/src/lib/services_specs.ts //go:generate go run tools/gen_dns_type_mapping.go -o internal/usecase/service_specs_dns_types.go +//go:generate go run tools/gen_caa_issuers.go -o web/src/lib/services/caa-issuers.json https://ccadb.my.salesforce-sites.com/ccadb/AllCAAIdentifiersReportCSVV2 //go:generate swag init --parseDependency --exclude internal/api-admin/ --generalInfo internal/api/route/route.go //go:generate swag init --parseDependency --output docs-admin --exclude internal/api/ --generalInfo internal/api-admin/route/route.go diff --git a/go.mod b/go.mod index 3a108c7c..7deabcd6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( git.happydns.org/checker-alias v0.1.0 git.happydns.org/checker-authoritative-consistency v0.1.0 git.happydns.org/checker-blacklist v0.1.0 + git.happydns.org/checker-caa v0.1.0 git.happydns.org/checker-dane v0.1.3 git.happydns.org/checker-dangling v0.1.0 git.happydns.org/checker-dav v0.1.0 diff --git a/go.sum b/go.sum index ccac6f0d..5aef1046 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ git.happydns.org/checker-authoritative-consistency v0.1.0 h1:+0XvJFC7tFVf0Dgruew git.happydns.org/checker-authoritative-consistency v0.1.0/go.mod h1:hPxEDSyrPq+KY9YU5QoZ1btecw/cU/Miouuacaz4wzk= git.happydns.org/checker-blacklist v0.1.0 h1:IV44Lxnw0dLBhoyAkAlq9A+hTB5B4RF4vLiW+nX21gg= git.happydns.org/checker-blacklist v0.1.0/go.mod h1:DRHkpULz8F6dKm0LUErAAQln0x8XByg+/UxbUY46oZk= +git.happydns.org/checker-caa v0.1.0 h1:L0kg9dqdJqmjaPrgbLtBvgEE6+e+7EVSSRPB5pIzNIQ= +git.happydns.org/checker-caa v0.1.0/go.mod h1:7ecPoFRYT0+Fl5DG17Xvz9Xh2alwgEpSSaE2rp0EcT0= git.happydns.org/checker-dane v0.1.3 h1:9VpQ4FrWJE/O6MZ08FCk1vmHsr3u5V7478als9Y4jl8= git.happydns.org/checker-dane v0.1.3/go.mod h1:md5SQA8M1QGq9MoXe3QVV+m55I+r8lU4iYx5KzvkbII= git.happydns.org/checker-dangling v0.1.0 h1:gZVyHAKG2U1FXBt7cPnZsr45JQWZ21jlThKhHckb+i8= diff --git a/tools/gen_caa_issuers.go b/tools/gen_caa_issuers.go new file mode 100644 index 00000000..ed6ab62a --- /dev/null +++ b/tools/gen_caa_issuers.go @@ -0,0 +1,405 @@ +// This file is part of the happyDomain (R) project. +// Copyright (c) 2022-2026 happyDomain +// Authors: Pierre-Olivier Mercier, et al. +// +// This program is offered under a commercial and under the AGPL license. +// For commercial licensing, contact us at . +// +// For AGPL licensing: +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//go:build ignore +// +build ignore + +package main + +import ( + "encoding/csv" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "os" + "sort" + "strings" +) + +const ( + subjectColumn = "Subject" + domainColumn = "Recognized CAA Domains" +) + +// row represents a single CCADB intermediate and the CAA domains it recognizes. +type row struct { + owner string + domains []string +} + +// score tracks how well a candidate owner matches a CAA domain across all +// rows that mention that domain. +type score struct { + rowCount int + minSize int + nameMatch bool +} + +func main() { + output := flag.String("o", "", "path to write the generated JSON file") + flag.Parse() + + if *output == "" { + fatal("missing required -o flag") + } + if flag.NArg() < 1 { + fatal("missing CCADB CSV URL (first positional argument)") + } + url := flag.Arg(0) + + rows, err := fetchAndParse(url) + if err != nil { + fatal(err.Error()) + } + + mapping := buildDomainToOwner(rows) + + if err := writeJSON(*output, mapping); err != nil { + fatal(err.Error()) + } +} + +func fetchAndParse(url string) ([]row, error) { + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("GET %s: %w", url, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("GET %s: unexpected status %s", url, resp.Status) + } + + return parseCSV(resp.Body) +} + +func parseCSV(r io.Reader) ([]row, error) { + reader := csv.NewReader(r) + reader.FieldsPerRecord = -1 + + header, err := reader.Read() + if err != nil { + return nil, fmt.Errorf("read header: %w", err) + } + + subjectIdx := indexOf(header, subjectColumn) + domainIdx := indexOf(header, domainColumn) + if subjectIdx < 0 { + return nil, fmt.Errorf("column %q not found in CSV header", subjectColumn) + } + if domainIdx < 0 { + return nil, fmt.Errorf("column %q not found in CSV header", domainColumn) + } + + var rows []row + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("read row: %w", err) + } + if subjectIdx >= len(record) || domainIdx >= len(record) { + continue + } + + owner := extractOrganization(record[subjectIdx]) + if owner == "" { + continue + } + + domains := splitDomains(record[domainIdx]) + if len(domains) == 0 { + continue + } + + rows = append(rows, row{owner: owner, domains: domains}) + } + return rows, nil +} + +// buildDomainToOwner inverts the CCADB rows into a CAA-domain → owner mapping. +// +// For each CAA identifier, the "authoritative" owner is selected by: +// 1. Preferring owners whose name contains a significant label of the CAA +// domain (e.g. "digicert.com" prefers "DigiCert, Inc." over cross-signed +// subordinates that also list digicert.com in their recognized set). +// 2. Then highest row count: real root CAs have many intermediates all +// recognizing their own identifier. +// 3. Then smallest minimum Recognized-CAA-Domains set: roots typically +// recognize just their own identifier, while subordinates inherit larger +// sets from cross-signing parents. +// 4. Alphabetical for determinism. +// +// Owner names are grouped case-insensitively to collapse CCADB casing +// inconsistencies (e.g. "Cloudflare, Inc." vs "CLOUDFLARE, INC."), preferring +// the variant with the fewest all-caps words. +func buildDomainToOwner(rows []row) map[string]string { + canonical := canonicalOwners(rows) + + scores := map[string]map[string]*score{} + + for _, r := range rows { + owner := canonical[strings.ToLower(r.owner)] + size := len(r.domains) + for _, dn := range r.domains { + if _, ok := scores[dn]; !ok { + scores[dn] = map[string]*score{} + } + s, ok := scores[dn][owner] + if !ok { + s = &score{minSize: size, nameMatch: ownerMatchesDomain(owner, dn)} + scores[dn][owner] = s + } + s.rowCount++ + if size < s.minSize { + s.minSize = size + } + } + } + + out := make(map[string]string, len(scores)) + for dn, byOwner := range scores { + var bestOwner string + var best *score + for owner, s := range byOwner { + if best == nil || scoreBetter(s, owner, best, bestOwner) { + best = s + bestOwner = owner + } + } + out[dn] = bestOwner + } + return out +} + +func scoreBetter(a *score, ao string, b *score, bo string) bool { + if a.nameMatch != b.nameMatch { + return a.nameMatch + } + if a.rowCount != b.rowCount { + return a.rowCount > b.rowCount + } + if a.minSize != b.minSize { + return a.minSize < b.minSize + } + return ao < bo +} + +// genericDomainLabels are labels that appear in CAA domains but don't identify +// a specific CA brand (e.g. "pki.goog" is Google, not "pki"). Anything shorter +// than 3 characters (TLDs) is also skipped. +var genericDomainLabels = map[string]bool{ + "com": true, "net": true, "org": true, "gov": true, "edu": true, + "co": true, + "pki": true, "tls": true, "ssl": true, "www": true, "eca": true, + "publicca": true, "epki": true, "cert": true, "trust": true, + "certificate": true, "ca": true, +} + +// ownerMatchesDomain returns true if a significant label of the CAA domain +// appears as a substring of the owner name (alphanumeric-only, lowercased). +// Used to prefer self-referential owners (e.g. "DigiCert" for "digicert.com") +// over cross-signed subordinates that also list the domain. +func ownerMatchesDomain(owner, caaDomain string) bool { + normName := alphaNumLower(owner) + for _, label := range strings.Split(caaDomain, ".") { + label = strings.ToLower(label) + if len(label) < 3 || genericDomainLabels[label] { + continue + } + if strings.Contains(normName, label) { + return true + } + } + return false +} + +func alphaNumLower(s string) string { + var b strings.Builder + b.Grow(len(s)) + for _, r := range s { + switch { + case r >= 'a' && r <= 'z': + b.WriteRune(r) + case r >= 'A' && r <= 'Z': + b.WriteRune(r + ('a' - 'A')) + case r >= '0' && r <= '9': + b.WriteRune(r) + } + } + return b.String() +} + +// canonicalOwners returns a map from lowercased owner name to the preferred +// display variant. Preference: fewer all-caps words, then lexicographically +// smallest (for determinism). +func canonicalOwners(rows []row) map[string]string { + variants := map[string]map[string]struct{}{} + for _, r := range rows { + key := strings.ToLower(r.owner) + if _, ok := variants[key]; !ok { + variants[key] = map[string]struct{}{} + } + variants[key][r.owner] = struct{}{} + } + + out := make(map[string]string, len(variants)) + for key, vs := range variants { + picks := make([]string, 0, len(vs)) + for v := range vs { + picks = append(picks, v) + } + sort.Slice(picks, func(i, j int) bool { + ai, aj := allCapsWords(picks[i]), allCapsWords(picks[j]) + if ai != aj { + return ai < aj + } + return picks[i] < picks[j] + }) + out[key] = picks[0] + } + return out +} + +// allCapsWords counts words (whitespace-delimited) that contain at least one +// letter and are entirely uppercase — a proxy for "ALL CAPS" shouting that we +// want to avoid when choosing a canonical display form. +func allCapsWords(s string) int { + n := 0 + for _, w := range strings.Fields(s) { + hasLetter := false + allUpper := true + for _, r := range w { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') { + hasLetter = true + if r >= 'a' && r <= 'z' { + allUpper = false + } + } + } + if hasLetter && allUpper { + n++ + } + } + return n +} + +func splitDomains(cell string) []string { + var out []string + for _, dn := range strings.FieldsFunc(cell, func(r rune) bool { + return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == '\r' + }) { + dn = strings.ToLower(strings.TrimSpace(dn)) + if isDomainName(dn) { + out = append(out, dn) + } + } + return out +} + +// isDomainName filters free-form text leaking from the CCADB "Recognized CAA +// Domains" cell (tokens like "None", "N/A", "Comma-separated", "list.", or +// sentence fragments). Requires at least two non-empty DNS-like labels and a +// TLD of at least two letters. +func isDomainName(s string) bool { + labels := strings.Split(s, ".") + if len(labels) < 2 { + return false + } + for _, l := range labels { + if l == "" || !isDomainLabel(l) { + return false + } + } + tld := labels[len(labels)-1] + if len(tld) < 2 { + return false + } + for _, r := range tld { + if r < 'a' || r > 'z' { + return false + } + } + return true +} + +func isDomainLabel(l string) bool { + if l[0] == '-' || l[len(l)-1] == '-' { + return false + } + for _, r := range l { + switch { + case r >= 'a' && r <= 'z': + case r >= '0' && r <= '9': + case r == '-': + default: + return false + } + } + return true +} + +func writeJSON(path string, data map[string]string) error { + buf, err := json.MarshalIndent(data, "", " ") + if err != nil { + return fmt.Errorf("marshal JSON: %w", err) + } + buf = append(buf, '\n') + + tmp := path + ".tmp" + if err := os.WriteFile(tmp, buf, 0o644); err != nil { + return fmt.Errorf("write %s: %w", tmp, err) + } + if err := os.Rename(tmp, path); err != nil { + return fmt.Errorf("rename %s -> %s: %w", tmp, path, err) + } + return nil +} + +// extractOrganization returns the O= (organization) value from an RFC-4514-ish +// DN string as provided by CCADB (fields separated by "; "). +func extractOrganization(subject string) string { + for _, field := range strings.Split(subject, "; ") { + field = strings.TrimSpace(field) + if v, ok := strings.CutPrefix(field, "O="); ok { + return strings.TrimSpace(v) + } + } + return "" +} + +func indexOf(header []string, name string) int { + for i, h := range header { + if strings.TrimSpace(h) == name { + return i + } + } + return -1 +} + +func fatal(msg string) { + fmt.Fprintln(os.Stderr, "gen_caa_issuers: "+msg) + os.Exit(1) +} diff --git a/web/src/lib/services/caa-issuers.json b/web/src/lib/services/caa-issuers.json new file mode 100644 index 00000000..9d189b7f --- /dev/null +++ b/web/src/lib/services/caa-issuers.json @@ -0,0 +1,146 @@ +{ + "1and1.digitalcertvalidation.com": "DigiCert, Inc.", + "accv.es": "ACCV", + "acme.jprs.jp": "Japan Registry Services Co., Ltd.", + "acme.trust.telia.com": "Telia Company AB", + "actalis.it": "Actalis S.p.A.", + "admin.ch": "Swiss Government PKI", + "affirmtrust.com": "AffirmTrust", + "almostfreessl.com": "SSLCom Group ltd.", + "amazon.com": "Amazon", + "amazonaws.com": "DigiCert, Inc.", + "amazontrust.com": "DigiCert, Inc.", + "anf.es": "ANF Autoridad de Certificacion", + "aoc.cat": "Agencia Catalana de Certificacio (NIF Q-0801176-I)", + "atos.net": "Atos", + "aws.amazon.com": "Amazon", + "awstrust.com": "DigiCert, Inc.", + "bjca.cn": "BEIJING CERTIFICATE AUTHORITY", + "buypass.com": "Buypass AS-983163327", + "buypass.no": "Buypass AS-983163327", + "camerfirma.com": "AC Camerfirma S.A.", + "certainly.com": "Certainly", + "certicamara.com": "CERTICAMARA S.A", + "certifyid.com": "WISeKey", + "certigna.com": "Certigna", + "certigna.fr": "Certigna", + "certsign.ro": "CERTSIGN SA", + "certum.eu": "Asseco Data Systems S.A.", + "certum.pl": "Asseco Data Systems S.A.", + "cfca.com.cn": "China Financial Certification Authority", + "cisco.com": "Cisco Systems", + "comodo.com": "COMODO CA Limited", + "comodoca.com": "COMODO CA Limited", + "comsign.co.il": "ComSign Ltd.", + "comsign.co.uk": "ComSign Ltd.", + "comsigneurope.com": "ComSign Ltd.", + "cybertrust.co.jp": "Cybertrust Japan Co., Ltd.", + "cybertrust.ne.jp": "Cybertrust Japan Co., Ltd.", + "d-trust.de": "D-Trust GmbH", + "d-trust.net": "D-Trust GmbH", + "darkmatter.ae": "DarkMatter LLC", + "desc.gov.ae": "UAE Government", + "dfn.de": "Deutsche Telekom Security GmbH", + "digicert.com": "DigiCert, Inc.", + "digicert.ne.jp": "DigiCert, Inc.", + "digitalcertvalidation.com": "DigiCert, Inc.", + "disig.sk": "Disig a.s.", + "docusign.fr": "OpenTrust", + "dtrust.de": "D-Trust GmbH", + "dvv.fi": "Digi- ja vaestotietovirasto CA", + "e-szigno.hu": "Microsec Ltd.", + "e-tugra.com": "E-Tugra EBG A.S.", + "e-tugra.com.tr": "E-Tugra EBG A.S.", + "eca.hinet.net": "Chunghwa Telecom Co., Ltd.", + "ecert.gov.hk": "Hongkong Post", + "edicomgroup.com": "EDICOM CAPITAL SL", + "elektronicznypodpis.pl": "Krajowa Izba Rozliczeniowa S.A.", + "emsign.com": "eMudhra Technologies Limited", + "entrust.net": "Entrust, Inc.", + "epki.com.tw": "Chunghwa Telecom Co., Ltd.", + "etugra.com": "E-Tugra EBG A.S.", + "etugra.com.tr": "E-Tugra EBG A.S.", + "fineid.fi": "Digi- ja vaestotietovirasto CA", + "firmaprofesional.com": "Firmaprofesional S.A.", + "fnmt.es": "FNMT-RCM", + "gca.nat.gov.tw": "行政院", + "gdca.com.cn": "Global Digital Cybersecurity Authority Co., Ltd.", + "geotrust.com": "GeoTrust Inc.", + "globalsign.com": "GlobalSign nv-sa", + "globaltrust.eu": "e-commerce monitoring GmbH", + "godaddy.com": "GoDaddy.com", + "gtlsca.nat.gov.tw": "行政院", + "harica.gr": "Hellenic Academic and Research Institutions CA", + "hightrusted.com": "WISeKey", + "hongkongpost.gov.hk": "Hongkong Post", + "identrust.com": "IdenTrust", + "imtrust.cn": "北京中科三方网络技术有限公司", + "intermediatecertificate.digitalcertvalidation.com": "DigiCert, Inc.", + "itrus.cn": "iTrusChina Co.,Ltd.", + "izenpe.com": "IZENPE S.A.", + "izenpe.eus": "IZENPE S.A.", + "jprs.jp": "Japan Registry Services Co., Ltd.", + "kamusm.gov.tr": "TUBITAK Kamu Sertifikasyon Merkezi", + "letsencrypt.org": "Let's Encrypt", + "microsoft.com": "Microsoft Corporation", + "msctrustgate.com": "MSC Trustgate.com Sdn. Bhd.", + "multicert.com": "MULTICERT - Serviços de Certificação Electrónica S.A.", + "navercloudtrust.com": "NAVER Cloud Trust Services Corp.", + "netlock.com": "NetLock Ltd.", + "netlock.eu": "NetLock Ltd.", + "netlock.hu": "NetLock Ltd.", + "netlock.net": "NetLock Ltd.", + "networksolutions.com": "Network Solutions L.L.C.", + "nrca.go.th": "National Telecom Public Company Limited", + "oaticerts.com": "Open Access Technology International Inc", + "oiste.org": "OISTE Foundation", + "pki.apple.com": "Apple Inc.", + "pki.dfn.de": "Deutsche Telekom Security GmbH", + "pki.eviden.com": "Eviden", + "pki.goog": "Google Trust Services", + "pki.hinet.net": "Chunghwa Telecom Co., Ltd.", + "pkiworks.com": "CommScope", + "postsignum.cz": "Česká pošta, s.p.", + "publicca.hinet.net": "Chunghwa Telecom Co., Ltd.", + "quovadisglobal.com": "DigiCert, Inc.", + "rapidssl.com": "DigiCert, Inc.", + "secomtrust.net": "SECOM Trust Systems CO.,LTD.", + "sectigo.com": "Sectigo Limited", + "sheca.com": "UniTrust", + "skidsolutions.eu": "AS Sertifitseerimiskeskus", + "solutissl.com": "Soluti", + "ssl.com": "SSL Corporation", + "ssl.gov.sa": "Baud Telecom Company", + "ssl.gpki.go.kr": "Ministry of the Interior and Safety", + "sslcomgroup.com": "SSLCom Group ltd.", + "starfieldtech.com": "Starfield Technologies, Inc.", + "startcomca.com": "StartCom CA", + "startssl.com": "StartCom Ltd.", + "stratossl.digitalcertvalidation.com": "DigiCert, Inc.", + "swisssign.com": "SwissSign AG", + "symantec.com": "Symantec Corporation", + "telesec.de": "Deutsche Telekom Security GmbH", + "telia.com": "Telia Company AB", + "telia.fi": "Telia Company AB", + "telia.se": "Telia Company AB", + "thawte.com": "DigiCert, Inc.", + "tls.hinet.net": "Chunghwa Telecom Co., Ltd.", + "trust-provider.com": "Sectigo Limited", + "trust.telia.com": "Telia Company AB", + "trustasia.com": "TrustAsia Technologies, Inc.", + "trustcor.ca": "TrustCor Systems S. de R.L.", + "trustfactory.net": "TrustFactory(Pty)Ltd", + "tuntrust.tn": "Agence Nationale de Certification Electronique", + "twca.com.tw": "TWCA", + "usertrust.com": "The USERTRUST Network", + "vincasign.net": "VINTEGRIS SL", + "visa.com": "VISA", + "volusion.digitalcertvalidation.com": "DigiCert, Inc.", + "web.com": "Network Solutions L.L.C.", + "wisekey.com": "WISeKey", + "wosign.com": "WoSign CA Limited", + "www.certinomis.com": "Certinomis", + "www.certinomis.fr": "Certinomis", + "www.digicert.com": "DigiCert, Inc.", + "www.identrust.com": "IdenTrust" +} diff --git a/web/src/lib/services/caa-issuers.ts b/web/src/lib/services/caa-issuers.ts index ddf2b3f5..95d1405f 100644 --- a/web/src/lib/services/caa-issuers.ts +++ b/web/src/lib/services/caa-issuers.ts @@ -19,167 +19,20 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -export const issuers: Record> = { - "Actalis S.p.A.": ["actalis.it"], - "Amazon Trust Services LLC": [ - "amazon.com", - "amazontrust.com", - "awstrust.com", - "amazonaws.com", - "aws.amazon.com", - ], - "ANF AC": ["anf.es"], - "Asseco Data Systems (formely Certum)": ["certum.pl", "certum.eu"], - Apple: ["pki.apple.com"], - Atos: ["atos.net"], - "Beijing CA": ["bjca.cn"], - "Buypass AS": ["buypass.com", "buypass.no"], - CAcert: ["cacert.org"], - "AC Camerfirma S.A.": ["camerfirma.com"], - CATCert: ["aoc.cat"], - "Fastly/Certainly": ["certainly.com"], - Certinomis: ["www.certinomis.com", "www.certinomis.fr"], - Dhimyotis: ["certigna.fr"], - Certizen: ["ecert.gov.hk", "hongkongpost.gov.hk"], - certSIGN: ["certsign.ro"], - "China Financial CA (CFCA)": ["cfca.com.cn"], - "China Internet Network Information Center (CNNIC)": ["cnnic.cn"], - "Chunghwa Telecom": [ - "pki.hinet.net", - "tls.hinet.net", - "eca.hinet.net", - "epki.com.tw", - "publicca.hinet.net", - ], - "ComSign Ltd": ["comsign.co.il", "comsign.co.uk", "comsigneurope.com"], - "Cybertrust Japan": ["cybertrust.ne.jp", "jcsinc.co.jp"], - "Deutsche Telekom Security": ["telesec.de"], - "DFN-PKI": ["pki.dfn.de", "dfn.de"], - DigiCert: [ - "digicert.com", - "symantec.com", - "geotrust.com", - "rapidssl.com", - "thawte.com", - "digitalcertvalidation.com", - "volusion.digitalcertvalidation.com", - "stratossl.digitalcertvalidation.com", - "intermediatecertificate.digitalcertvalidation.com", - "1and1.digitalcertvalidation.com", - ], - "DigitalSign CD": ["digitalsign.pt"], - "Disig, a.s.": ["disig.sk"], - DocuSign: ["docusign.fr"], - "D-Trust GmbH": ["dtrust.de", "d-trust.de", "dtrust.net", "d-trust.net"], - DigitalTrust: ["digitaltrust.ae"], - EDICOM: ["edicomgroup.com"], - "eMudhra Technologies Limited": ["emsign.com"], - Entrust: ["entrust.net", "affirmtrust.com"], - "E-TUGRA Inc.": ["e-tugra.com", "e-tugra.com.tr", "etugra.com", "etugra.com.tr"], - "AC Firmaprofesional CIF": ["firmaprofesional.com"], - "Guang Dong CA Co.": ["gdca.com.cn"], - GlobalSign: ["globalsign.com"], - "Global Trust": ["globaltrust.eu"], - "GoDaddy Inc.": ["godaddy.com", "starfieldtech.com"], - "Google Trust Services (GTS)": ["pki.goog", "google.com"], - "ACCV (Spain gov)": ["accv.es"], - "FNMT (Spain gov)": ["fnmt.es"], - "Agence Nationale de Certification Electronique (Tunisia gov)": ["tuntrust.tn"], - GRCA: ["gca.nat.gov.tw"], - HARICA: ["harica.gr"], - "Hongkong Post": ["ecert.gov.hk", "hongkongpost.gov.hk"], - IdenTrust: ["identrust.com", "www.identrust.com"], - "iTrusChina Co.": ["itrus.com.cn", "itrus.cn"], - "IZENPE S.A.": ["izenpe.com", "izenpe.eus"], - "Japan Registry Services": ["jprs.co.jp"], - "Kamu Sertifikasyon Merkezi": ["kamusm.gov.tr"], - "KPN Corporate Market BV": ["kpn.com"], - "Krajowa Izba Rozliczeniowa S.A. (KIR)": ["elektronicznypodpis.pl"], - LAWtrust: ["lawtrust.co.za"], - "Let's Encrypt": ["letsencrypt.org"], - "Logius PKIoverheid": ["logius.nl"], - "Microsec Ltd.": ["e-szigno.hu"], - Microsoft: ["microsoft.com"], - "Microsoft IT": ["ssladmin.microsoft.com"], - "MSC Trustgate": ["msctrustgate.com"], - "National Center for Digital Certification (NCDC)": ["ncdc.gov.sa"], - "NAVER Business Platform Corp.": ["certificate.naver.com"], - "NetLock Kft.": ["netlock.hu", "netlock.net", "netlock.eu"], - Networking4all: ["trustproviderbv.digitalcertvalidation.com"], - "Network Solutions LLC": ["networksolutions.com", "web.com"], - "OISTE Foundation": ["wisekey.com", "hightrusted.com", "certifyid.com", "oiste.org"], - "Open Access Technology International": ["oati.com"], - "Prvni certifikacni autorita, a.s.": ["ica.cz"], - PKIoverheid: ["www.pkioverheid.nl"], - QuoVadis: [ - "quovadisglobal.com", - "digicert.com", - "digicert.ne.jp", - "cybertrust.ne.jp", - "symantec.com", - "thawte.com", - "geotrust.com", - "rapidssl.com", - "digitalcertvalidation.com", - ], - "SECOM Trust Systems": ["secomtrust.net"], - Sectigo: ["sectigo.com", "comodo.com", "comodoca.com", "usertrust.com", "trust-provider.com"], - "Shanghai Electronic Certification Authority Co. Ltd": [ - "sheca.com", - "imtrust.cn", - "wwwtrust.cn", - ], - "SK ID Solutions AS": ["skidsolutions.eu"], - "SSL Corporation": ["ssl.com"], - "Skaitmeninio sertifikavimo centras (SSC)": ["ssc.lt"], - "SwissSign AG": [ - "swisssign.com", - "swisssign.net", - "swissign.com", - "swisssign.ch", - "swisssign.li", - "swissign.li", - "swisssign.org", - "swisssign.biz", - "swisstsa.ch", - "swisstsa.li", - "digitalid.ch", - "digital-id.ch", - "zert.ch", - "rootsigning.com", - "root-signing.ch", - "ssl-certificate.ch", - "managed-pki.ch", - "managed-pki.de", - "swissstick.com", - "swisssigner.ch", - "pki-posta.ch", - "pki-poste.ch", - "pki-post.ch", - "trustdoc.ch", - "trustsign.ch", - "swisssigner.com", - "postsuisseid.ch", - "suisseid-service.ch", - "signdemo.com", - "sirb.com", - ], - "SecureTrust Corporation": ["trustwave.com", "securetrust.com"], - "TAIWAN-CA Inc. (TWCA)": ["twca.com.tw"], - "Telia Finland Oyj": ["telia.com", "telia.fi", "telia.se"], - "TrustCor Systems": ["trustcor.ca"], - "T-Systems Enterprise Services": ["t-systems.com"], - Visa: ["visa.com"], - Zertificon: ["zertificon.com"], - "360": ["browser.360.cn"], -}; +import data from "./caa-issuers.json"; -export const rev_issuers: Record = {}; +export const rev_issuers: Record = data; -for (const issuer in issuers) { - for (const dn of issuers[issuer]) { - rev_issuers[dn] = issuer; - } +export const issuers: Record> = {}; + +for (const dn in rev_issuers) { + const owner = rev_issuers[dn]; + if (!issuers[owner]) issuers[owner] = []; + issuers[owner].push(dn); +} + +for (const owner in issuers) { + issuers[owner].sort(); } export default issuers;