Publish certificate chain data for DANE consumers
Add Chain []CertInfo to TLSProbe, carrying per-cert DER and precomputed TLSA hashes (Cert/SPKI, SHA-256/SHA-512) plus the raw SPKI DER. This lets downstream checkers (checker-dane) perform TLSA matching against the observed chain without re-running a TLS handshake.
This commit is contained in:
parent
d61a81d51c
commit
f4cfd13412
2 changed files with 62 additions and 0 deletions
|
|
@ -2,8 +2,11 @@ package checker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -15,6 +18,31 @@ import (
|
||||||
"git.happydns.org/checker-tls/contract"
|
"git.happydns.org/checker-tls/contract"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// buildChain returns CertInfo for each cert presented by the server, in the
|
||||||
|
// order the server sent them (leaf first). SPKI is extracted from the parsed
|
||||||
|
// certificate's RawSubjectPublicKeyInfo so we hash exactly the DER bytes
|
||||||
|
// DANE selector 1 refers to (RFC 6698 §1.1.3).
|
||||||
|
func buildChain(certs []*x509.Certificate) []CertInfo {
|
||||||
|
out := make([]CertInfo, len(certs))
|
||||||
|
for i, c := range certs {
|
||||||
|
certSum256 := sha256.Sum256(c.Raw)
|
||||||
|
certSum512 := sha512.Sum512(c.Raw)
|
||||||
|
spkiSum256 := sha256.Sum256(c.RawSubjectPublicKeyInfo)
|
||||||
|
spkiSum512 := sha512.Sum512(c.RawSubjectPublicKeyInfo)
|
||||||
|
out[i] = CertInfo{
|
||||||
|
DERBase64: base64.StdEncoding.EncodeToString(c.Raw),
|
||||||
|
Subject: c.Subject.String(),
|
||||||
|
Issuer: c.Issuer.String(),
|
||||||
|
CertSHA256: hex.EncodeToString(certSum256[:]),
|
||||||
|
CertSHA512: hex.EncodeToString(certSum512[:]),
|
||||||
|
SPKISHA256: hex.EncodeToString(spkiSum256[:]),
|
||||||
|
SPKISHA512: hex.EncodeToString(spkiSum512[:]),
|
||||||
|
SPKIDERBase64: base64.StdEncoding.EncodeToString(c.RawSubjectPublicKeyInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// probeTypeString renders the TLSProbe.Type string from a TLSEndpoint.
|
// probeTypeString renders the TLSProbe.Type string from a TLSEndpoint.
|
||||||
// Observation consumers already parse this field in its "tls" /
|
// Observation consumers already parse this field in its "tls" /
|
||||||
// "starttls-<proto>" shape; the contract-level split of direct vs.
|
// "starttls-<proto>" shape; the contract-level split of direct vs.
|
||||||
|
|
@ -102,6 +130,7 @@ func probe(ctx context.Context, ep contract.TLSEndpoint, timeout time.Duration)
|
||||||
}
|
}
|
||||||
p.Subject = leaf.Subject.CommonName
|
p.Subject = leaf.Subject.CommonName
|
||||||
p.DNSNames = append(p.DNSNames, leaf.DNSNames...)
|
p.DNSNames = append(p.DNSNames, leaf.DNSNames...)
|
||||||
|
p.Chain = buildChain(state.PeerCertificates)
|
||||||
|
|
||||||
hostnameMatch := leaf.VerifyHostname(sni) == nil
|
hostnameMatch := leaf.VerifyHostname(sni) == nil
|
||||||
p.HostnameMatch = &hostnameMatch
|
p.HostnameMatch = &hostnameMatch
|
||||||
|
|
|
||||||
|
|
@ -59,11 +59,44 @@ type TLSProbe struct {
|
||||||
IssuerAKI string `json:"issuer_aki,omitempty"`
|
IssuerAKI string `json:"issuer_aki,omitempty"`
|
||||||
Subject string `json:"subject,omitempty"`
|
Subject string `json:"subject,omitempty"`
|
||||||
DNSNames []string `json:"dns_names,omitempty"`
|
DNSNames []string `json:"dns_names,omitempty"`
|
||||||
|
// Chain carries one entry per certificate presented by the server
|
||||||
|
// (leaf first, then intermediates in order). Each entry precomputes
|
||||||
|
// the four TLSA selector×matching_type hashes plus the raw DER so
|
||||||
|
// DANE consumers can match without re-handshaking or re-parsing.
|
||||||
|
Chain []CertInfo `json:"chain,omitempty"`
|
||||||
ElapsedMS int64 `json:"elapsed_ms,omitempty"`
|
ElapsedMS int64 `json:"elapsed_ms,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Issues []Issue `json:"issues,omitempty"`
|
Issues []Issue `json:"issues,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CertInfo describes one certificate in the presented chain together with
|
||||||
|
// pre-hashed forms suitable for DANE/TLSA matching (RFC 6698 §2.1).
|
||||||
|
//
|
||||||
|
// Hex fields are lowercase, matching the representation emitted by
|
||||||
|
// miekg/dns for TLSA RR Certificate fields.
|
||||||
|
type CertInfo struct {
|
||||||
|
// DERBase64 is the standard base64 encoding of the certificate's DER
|
||||||
|
// form. Carried so consumers can do matching-type 0 (Full) without
|
||||||
|
// requiring a precomputed "full" hash and for fallback inspection.
|
||||||
|
DERBase64 string `json:"der_base64,omitempty"`
|
||||||
|
|
||||||
|
// Subject / Issuer are short human-readable DNs for the HTML report.
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Issuer string `json:"issuer,omitempty"`
|
||||||
|
|
||||||
|
// Selector 0 = full certificate.
|
||||||
|
CertSHA256 string `json:"cert_sha256,omitempty"`
|
||||||
|
CertSHA512 string `json:"cert_sha512,omitempty"`
|
||||||
|
|
||||||
|
// Selector 1 = SubjectPublicKeyInfo.
|
||||||
|
SPKISHA256 string `json:"spki_sha256,omitempty"`
|
||||||
|
SPKISHA512 string `json:"spki_sha512,omitempty"`
|
||||||
|
|
||||||
|
// SPKIDERBase64 lets consumers handle (selector=1, matching=0) without
|
||||||
|
// re-parsing the certificate.
|
||||||
|
SPKIDERBase64 string `json:"spki_der_base64,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Issue is a single TLS finding surfaced to the consumer.
|
// Issue is a single TLS finding surfaced to the consumer.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue