package checker import ( "encoding/json" "net" "strconv" "time" sdk "git.happydns.org/checker-sdk-go/checker" ) // tlsProbeView is the local, permissive view of a single checker-tls // probe payload. We decode only what the CAA rule needs; unknown fields // are ignored so the TLS checker can evolve its schema independently. // // The IssuerAKI / IssuerDN fields are the cross-checker contract the // CAA rule depends on. They were added to checker-tls so each probe // carries the issuer identity in a form that maps directly to the // CCADB "CAA Identifiers" CSV. type tlsProbeView struct { Host string `json:"host,omitempty"` Port uint16 `json:"port,omitempty"` Endpoint string `json:"endpoint,omitempty"` Type string `json:"type,omitempty"` Issuer string `json:"issuer,omitempty"` IssuerDN string `json:"issuer_dn,omitempty"` IssuerAKI string `json:"issuer_aki,omitempty"` NotAfter time.Time `json:"not_after,omitempty"` ChainValid *bool `json:"chain_valid,omitempty"` } // address returns "host:port" as a human-readable identifier for // Issue.Endpoint when the upstream Endpoint field is missing. func (v *tlsProbeView) address() string { if v.Endpoint != "" { return v.Endpoint } if v.Host != "" && v.Port != 0 { return net.JoinHostPort(v.Host, strconv.FormatUint(uint64(v.Port), 10)) } return v.Host } // parseTLSRelated decodes a single RelatedObservation into a list of // probes. Two payload shapes are supported to match the dual-shape // contract checker-xmpp already consumes: // // 1. {"probes": {"": , …}}: the current checker-tls // format. When r.Ref is set and present in the map, only that // entry is returned; otherwise all probes are returned so a rule // operating at domain scope can still see them. // 2. : a single top-level probe, kept for back-compat. // // Returns nil when the payload is not a recognizable probe shape. func parseTLSRelated(r sdk.RelatedObservation) []*tlsProbeView { var keyed struct { Probes map[string]tlsProbeView `json:"probes"` } if err := json.Unmarshal(r.Data, &keyed); err == nil && keyed.Probes != nil { if r.Ref != "" { if p, ok := keyed.Probes[r.Ref]; ok { cp := p return []*tlsProbeView{&cp} } } out := make([]*tlsProbeView, 0, len(keyed.Probes)) for _, p := range keyed.Probes { cp := p out = append(out, &cp) } return out } var v tlsProbeView if err := json.Unmarshal(r.Data, &v); err != nil { return nil } if v.Host == "" && v.IssuerAKI == "" && v.IssuerDN == "" { return nil } return []*tlsProbeView{&v} } // parseAllTLSRelated flattens a slice of RelatedObservations into the // full set of probe views they carry. This is the input the rule works // from; one entry per endpoint, not per observation. func parseAllTLSRelated(related []sdk.RelatedObservation) []*tlsProbeView { var out []*tlsProbeView for _, r := range related { out = append(out, parseTLSRelated(r)...) } return out }