diff --git a/services/caa.go b/services/caa.go new file mode 100644 index 0000000..9fe1842 --- /dev/null +++ b/services/caa.go @@ -0,0 +1,269 @@ +// Copyright or © or Copr. happyDNS (2023) +// +// contact@happydomain.org +// +// This software is a computer program whose purpose is to provide a modern +// interface to interact with DNS systems. +// +// This software is governed by the CeCILL license under French law and abiding +// by the rules of distribution of free software. You can use, modify and/or +// redistribute the software under the terms of the CeCILL license as +// circulated by CEA, CNRS and INRIA at the following URL +// "http://www.cecill.info". +// +// As a counterpart to the access to the source code and rights to copy, modify +// and redistribute granted by the license, users are provided only with a +// limited warranty and the software's author, the holder of the economic +// rights, and the successive licensors have only limited liability. +// +// In this respect, the user's attention is drawn to the risks associated with +// loading, using, modifying and/or developing or reproducing the software by +// the user in light of its specific status of free software, that may mean +// that it is complicated to manipulate, and that also therefore means that it +// is reserved for developers and experienced professionals having in-depth +// computer knowledge. Users are therefore encouraged to load and test the +// software's suitability as regards their requirements in conditions enabling +// the security of their systems and/or data to be ensured and, more generally, +// to use and operate it in the same conditions as regards security. +// +// The fact that you are presently reading this means that you have had +// knowledge of the CeCILL license and that you accept its terms. + +package svcs + +import ( + "net/url" + "strings" + + "github.com/StackExchange/dnscontrol/v4/models" + "github.com/miekg/dns" + + "git.happydns.org/happyDomain/model" + "git.happydns.org/happyDomain/services/common" + "git.happydns.org/happyDomain/utils" +) + +type CAAParameter struct { + Tag string + Value string +} + +type CAAIssueValue struct { + IssuerDomainName string + Parameters []CAAParameter +} + +func parseIssueValue(value string) (ret CAAIssueValue) { + tmp := strings.Split(value, ";") + ret.IssuerDomainName = strings.TrimSpace(tmp[0]) + + for _, param := range tmp[1:] { + tmpparam := strings.SplitN(param, "=", 2) + ret.Parameters = append(ret.Parameters, CAAParameter{ + Tag: strings.TrimSpace(tmpparam[0]), + Value: strings.TrimSpace(tmpparam[1]), + }) + } + + return +} + +func (v *CAAIssueValue) String() string { + var b strings.Builder + + b.WriteString(v.IssuerDomainName) + + if len(v.Parameters) > 0 { + b.WriteString(";") + + for _, param := range v.Parameters { + b.WriteString(param.Tag) + b.WriteString("=") + b.WriteString(param.Value) + } + } + + return b.String() +} + +type CAA struct { + DisallowIssue bool + Issue []CAAIssueValue + DisallowWildcardIssue bool + IssueWild []CAAIssueValue + Iodef []*common.URL +} + +func (s *CAA) GetNbResources() int { + nb := 0 + + if s.DisallowIssue { + nb += 1 + } else { + nb += len(s.Issue) + if s.DisallowWildcardIssue { + nb += 1 + } else { + nb += len(s.IssueWild) + } + } + + return nb + len(s.Iodef) +} + +func (s *CAA) GenComment(origin string) string { + if s.DisallowIssue { + return "Certificate issuance disallowed" + } else { + var issuance []string + for _, iss := range s.Issue { + issuance = append(issuance, iss.IssuerDomainName) + } + + ret := strings.Join(issuance, ", ") + + if s.DisallowWildcardIssue { + if ret != "" { + ret += "; " + } + ret += "Wildcard issuance disallowed" + } else if len(s.IssueWild) > 0 { + if ret != "" { + ret += "; wildcard: " + } + + var issuancew []string + for _, iss := range s.IssueWild { + issuancew = append(issuancew, iss.IssuerDomainName) + } + + ret += strings.Join(issuancew, ", ") + } + + return ret + } +} + +func (s *CAA) GenRRs(domain string, ttl uint32, origin string) (rrs models.Records) { + if s.DisallowIssue { + rc := utils.NewRecordConfig(domain, "CAA", ttl, origin) + rc.CaaFlag = 0 + rc.CaaTag = "issue" + rc.SetTarget(";") + + rrs = append(rrs, rc) + } else { + for _, issue := range s.Issue { + rc := utils.NewRecordConfig(domain, "CAA", ttl, origin) + rc.CaaFlag = 0 + rc.CaaTag = "issue" + rc.SetTarget(issue.String()) + + rrs = append(rrs, rc) + } + + if s.DisallowWildcardIssue { + rc := utils.NewRecordConfig(domain, "CAA", ttl, origin) + rc.CaaFlag = 0 + rc.CaaTag = "issuewild" + rc.SetTarget(";") + + rrs = append(rrs, rc) + } else { + for _, issue := range s.IssueWild { + rc := utils.NewRecordConfig(domain, "CAA", ttl, origin) + rc.CaaFlag = 0 + rc.CaaTag = "issuewild" + rc.SetTarget(issue.String()) + + rrs = append(rrs, rc) + } + } + } + + if len(s.Iodef) > 0 { + for _, iodef := range s.Iodef { + rc := utils.NewRecordConfig(domain, "CAA", ttl, origin) + rc.CaaFlag = 0 + rc.CaaTag = "iodef" + rc.SetTarget(iodef.String()) + + rrs = append(rrs, rc) + } + } + + return +} + +func caa_analyze(a *Analyzer) (err error) { + pool := map[string]*CAA{} + + for _, record := range a.SearchRR(AnalyzerRecordFilter{Type: dns.TypeCAA}) { + domain := record.NameFQDN + + if record.Type == "CAA" { + if _, ok := pool[domain]; !ok { + pool[domain] = &CAA{} + } + + analyzed := pool[domain] + + if record.CaaTag == "issue" { + value := record.GetTargetField() + if value == ";" { + analyzed.DisallowIssue = true + } else { + analyzed.Issue = append(analyzed.Issue, parseIssueValue(value)) + } + } + + if record.CaaTag == "issuewild" { + value := record.GetTargetField() + if value == ";" { + analyzed.DisallowWildcardIssue = true + } else { + analyzed.IssueWild = append(analyzed.IssueWild, parseIssueValue(value)) + } + } + + if record.CaaTag == "iodef" { + if u, err := url.Parse(record.GetTargetField()); err != nil { + continue + } else { + tmp := common.URL(*u) + analyzed.Iodef = append(analyzed.Iodef, &tmp) + } + } + + err = a.UseRR(record, domain, pool[domain]) + if err != nil { + return + } + } + } + + return nil +} + +func init() { + RegisterService( + func() happydns.Service { + return &CAA{} + }, + caa_analyze, + ServiceInfos{ + Name: "Certification Authority Authorization (CAA)", + Description: "Indicate to certificate authorities whether they are authorized to issue digital certificates for a particular domain name.", + Categories: []string{ + "tls", + }, + Restrictions: ServiceRestrictions{ + Single: true, + NeedTypes: []uint16{ + dns.TypeCAA, + }, + }, + }, + 1, + ) +} diff --git a/services/common/url.go b/services/common/url.go new file mode 100644 index 0000000..01804dc --- /dev/null +++ b/services/common/url.go @@ -0,0 +1,59 @@ +// Copyright or © or Copr. happyDNS (2023) +// +// contact@happydomain.org +// +// This software is a computer program whose purpose is to provide a modern +// interface to interact with DNS systems. +// +// This software is governed by the CeCILL license under French law and abiding +// by the rules of distribution of free software. You can use, modify and/or +// redistribute the software under the terms of the CeCILL license as +// circulated by CEA, CNRS and INRIA at the following URL +// "http://www.cecill.info". +// +// As a counterpart to the access to the source code and rights to copy, modify +// and redistribute granted by the license, users are provided only with a +// limited warranty and the software's author, the holder of the economic +// rights, and the successive licensors have only limited liability. +// +// In this respect, the user's attention is drawn to the risks associated with +// loading, using, modifying and/or developing or reproducing the software by +// the user in light of its specific status of free software, that may mean +// that it is complicated to manipulate, and that also therefore means that it +// is reserved for developers and experienced professionals having in-depth +// computer knowledge. Users are therefore encouraged to load and test the +// software's suitability as regards their requirements in conditions enabling +// the security of their systems and/or data to be ensured and, more generally, +// to use and operate it in the same conditions as regards security. +// +// The fact that you are presently reading this means that you have had +// knowledge of the CeCILL license and that you accept its terms. + +package common + +import ( + "encoding/json" + "net/url" +) + +type URL url.URL + +func (u URL) String() string { + tmp := url.URL(u) + return (&tmp).String() +} + +func (u URL) MarshalJSON() ([]byte, error) { + tmp := url.URL(u) + return json.Marshal((&tmp).String()) +} + +func (u *URL) UnmarshalJSON(b []byte) error { + var v string + if err := json.Unmarshal(b, &v); err != nil { + return err + } + tmp, err := url.Parse(v) + *u = URL(*tmp) + return err +} diff --git a/ui/src/lib/api/service_specs.ts b/ui/src/lib/api/service_specs.ts index 489401d..c9fb9b0 100644 --- a/ui/src/lib/api/service_specs.ts +++ b/ui/src/lib/api/service_specs.ts @@ -10,7 +10,7 @@ export async function listServiceSpecs(): Promise> } export async function getServiceSpec(ssid: string): Promise { - if (ssid == "string") { + if (ssid == "string" || ssid == "common.URL") { return Promise.resolve({fields: null}); } else { const res = await fetch(`/api/service_specs/` + ssid, { diff --git a/ui/src/lib/components/ResourceInput.svelte b/ui/src/lib/components/ResourceInput.svelte index ff75bfc..c4aa817 100644 --- a/ui/src/lib/components/ResourceInput.svelte +++ b/ui/src/lib/components/ResourceInput.svelte @@ -2,6 +2,7 @@ import { createEventDispatcher } from 'svelte'; import BasicInput from '$lib/components/resources/basic.svelte'; + import CAAForm from '$lib/components/resources/CAA.svelte'; import MapInput from '$lib/components/resources/map.svelte'; import ObjectInput from '$lib/components/resources/object.svelte'; import RawInput from '$lib/components/resources/raw.svelte'; @@ -45,6 +46,16 @@ type={sanitizeType(type)} bind:value={value} /> +{:else if type == "svcs.CAA"} + dispatch("delete-this-service", event.detail)} + on:update-this-service={(event) => dispatch("update-this-service", event.detail)} + /> {:else if typeof value === 'object' || Array.isArray(specs)} + import { createEventDispatcher } from 'svelte'; + + import { + Button, + Icon, + Input, + } from 'sveltestrap'; + + const dispatch = createEventDispatcher(); + + export let newone = false; + export let readonly = false; + export let value: any; + + let kind: string = "web"; + let url: string; + + $: if (value) switch (value.split(":")[0]) { + case "mailto": + kind = "mail"; + url = value.split(":")[1]; + break; + default: + kind = "web"; + url = value; + } + + function updateValue(url) { + if (kind == "mail") { + value = "mailto:" + url; + } else { + value = url; + } + } + + $: updateValue(url); + + +
+ + + + + + + + {#if !newone} + + {:else} + + {/if} +
diff --git a/ui/src/lib/components/resources/CAA-issuer.svelte b/ui/src/lib/components/resources/CAA-issuer.svelte new file mode 100644 index 0000000..ac95872 --- /dev/null +++ b/ui/src/lib/components/resources/CAA-issuer.svelte @@ -0,0 +1,106 @@ + + +
+ {#if (newone && value.IssuerDomainName == "") || rev_issuers[value.IssuerDomainName]} + + {#each Object.keys(issuers) as issuer} + + {/each} + + + {:else} + + {/if} + {#if !newone} + + {:else} + + {/if} +
+{#if !newone} +
+ {#if value.Parameters} + {#each value.Parameters as parameter, k} + + {#if parameter.edit} +
parameter.edit = false} + > + + = + + +
+ {:else} + parameter.edit = true} + > + {parameter.Tag}={parameter.Value} + + {value.Parameters.splice(k, 1); value = value;}} + > + + + {/if} +
+ {/each} + {/if} + {if (value.Parameters == null) value.Parameters = []; value.Parameters.push({Tag:"", Value: "", edit: true}); value = value;}} + > + Add parameter + +
+{/if} diff --git a/ui/src/lib/components/resources/CAA-issuers.ts b/ui/src/lib/components/resources/CAA-issuers.ts new file mode 100644 index 0000000..343bf7e --- /dev/null +++ b/ui/src/lib/components/resources/CAA-issuers.ts @@ -0,0 +1,354 @@ +export const issuers = { + "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" + ] +}; + +export const rev_issuers: Record = { }; + +for (const issuer in issuers) { + for (const dn of issuers[issuer]) { + rev_issuers[dn] = issuer; + } +} + +export default issuers; diff --git a/ui/src/lib/components/resources/CAA.svelte b/ui/src/lib/components/resources/CAA.svelte new file mode 100644 index 0000000..1dfb638 --- /dev/null +++ b/ui/src/lib/components/resources/CAA.svelte @@ -0,0 +1,139 @@ + + +

Certificates issuance

+ + + + + +
+ Authorized Issuers +
+ +{#if !value.DisallowIssue} +
    + {#if value.Issue} + {#each value.Issue as issue, k} +
  • + {value.Issue.splice(k, 1); value = value;}} + /> +
  • + {/each} + {:else} + + All issuer authorized. With those parameters, all issuer can create certificate for this domain and subdomain. + + {/if} + {#if !readonly} +
  • + {if (!value.Issue) value.Issue = []; value.Issue.push(e.detail); value = value;}} + /> +
  • + {/if} +
+{:else} + + No issuer authorized. With those parameters, no issuer is allowed to create certificate for this subdomain. + +{/if} + +

Wildcard certificates issuance

+ + + + + +
+ Authorized Issuers +
+ +{#if !value.DisallowWildcardIssue} +
    + {#if value.IssueWild} + {#each value.IssueWild as issue, k} +
  • + {value.IssueWild.splice(k, 1); value = value;}} + /> +
  • + {/each} + {:else if value.DisallowIssue} + + No issuer authorized. With those parameters, no issuer is authorized to create wildcard certificate for this domain and subdomain. But this can be override with the following settings: + + {:else if value.Issue} + + Same as regular certificate issuance. With those parameters, all issuer authorized for certificate issuance can also create wildcard certificate for this domain and subdomain. + + {:else} + + All issuer authorized. With those parameters, all issuer can create wildcard certificate for this domain and subdomain. + + {/if} + {#if !readonly} +
  • + {if (!value.IssueWild) value.IssueWild = []; value.IssueWild.push(e.detail); value = value;}} + /> +
  • + {/if} +
+{:else} + + No wildcard issuer authorized. With those parameters, no issuer is allowed to create wildcard certificate for this subdomain. + +{/if} + +

Incident Response

+ +

+ How would you want to be contacted in case of violation of the current security policy? +

+ +{#if value.Iodef} + {#each value.Iodef as iodef, k} + + {/each} +{/if} +{#if !readonly} + +{/if} diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 9cdccf2..9f0bf45 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -8,6 +8,6 @@ export function fillUndefinedValues(value: any, spec: Field) { if (spec.default !== undefined) value[spec.id] = spec.default; else if (vartype == "[]uint8") value[spec.id] = ""; else if (vartype.startsWith("[]")) value[spec.id] = []; - else if (vartype != "string" && !vartype.startsWith("uint") && !vartype.startsWith("int") && vartype != "net.IP" && vartype != "time.Duration" && vartype != "common.Duration") value[spec.id] = { }; + else if (vartype != "string" && !vartype.startsWith("uint") && !vartype.startsWith("int") && vartype != "net.IP" && vartype != "common.URL" && vartype != "time.Duration" && vartype != "common.Duration") value[spec.id] = { }; } }