// Copyright (C) 2023 Opsmate, Inc. // // This Source Code Form is subject to the terms of the Mozilla // Public License, v. 2.0. If a copy of the MPL was not distributed // with this file, You can obtain one at http://mozilla.org/MPL/2.0/. // // This software is distributed WITHOUT A WARRANTY OF ANY KIND. // See the Mozilla Public License for details. package monitor import ( "bytes" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "strings" "time" "software.sslmate.com/src/certspotter" "software.sslmate.com/src/certspotter/ct" ) type discoveredCert struct { WatchItem WatchItem LogEntry *logEntry Info *certspotter.CertInfo Chain []ct.ASN1Cert // first entry is the leaf certificate or precertificate TBSSHA256 [32]byte // computed over Info.TBS.Raw SHA256 [32]byte // computed over Chain[0] PubkeySHA256 [32]byte // computed over Info.TBS.PublicKey.FullBytes Identifiers *certspotter.Identifiers CertPath string // empty if not saved on the filesystem JSONPath string // empty if not saved on the filesystem TextPath string // empty if not saved on the filesystem } func (cert *discoveredCert) pemChain() []byte { var buffer bytes.Buffer for _, certBytes := range cert.Chain { if err := pem.Encode(&buffer, &pem.Block{ Type: "CERTIFICATE", Bytes: certBytes, }); err != nil { panic(fmt.Errorf("encoding certificate as PEM failed unexpectedly: %w", err)) } } return buffer.Bytes() } func (cert *discoveredCert) json() []byte { object := map[string]any{ "tbs_sha256": hex.EncodeToString(cert.TBSSHA256[:]), "cert_sha256": hex.EncodeToString(cert.SHA256[:]), "pubkey_sha256": hex.EncodeToString(cert.PubkeySHA256[:]), "issuer_der": cert.Info.TBS.Issuer.FullBytes, "subject_der": cert.Info.TBS.Subject.FullBytes, "dns_names": cert.Identifiers.DNSNames, "ip_addresses": cert.Identifiers.IPAddrs, } if cert.Info.ValidityParseError == nil { object["not_before"] = cert.Info.Validity.NotBefore object["not_after"] = cert.Info.Validity.NotAfter } else { object["not_before"] = nil object["not_after"] = nil } if cert.Info.SerialNumberParseError == nil { object["serial_number"] = fmt.Sprintf("%x", cert.Info.SerialNumber) } else { object["serial_number"] = nil } jsonBytes, err := json.Marshal(object) if err != nil { panic(fmt.Errorf("encoding certificate as JSON failed unexpectedly: %w", err)) } return jsonBytes } func (cert *discoveredCert) save() error { if err := writeFile(cert.CertPath, cert.pemChain(), 0666); err != nil { return err } if err := writeFile(cert.JSONPath, cert.json(), 0666); err != nil { return err } if err := writeFile(cert.TextPath, []byte(cert.Text()), 0666); err != nil { return err } return nil } func (cert *discoveredCert) Environ() []string { env := []string{ "EVENT=discovered_cert", "SUMMARY=certificate discovered for " + cert.WatchItem.String(), "CERT_PARSEABLE=yes", // backwards compat with pre-0.15.0; not documented "LOG_URI=" + cert.LogEntry.Log.URL, "ENTRY_INDEX=" + fmt.Sprint(cert.LogEntry.Index), "WATCH_ITEM=" + cert.WatchItem.String(), "TBS_SHA256=" + hex.EncodeToString(cert.TBSSHA256[:]), "CERT_SHA256=" + hex.EncodeToString(cert.SHA256[:]), "FINGERPRINT=" + hex.EncodeToString(cert.SHA256[:]), // backwards compat with pre-0.15.0; not documented "PUBKEY_SHA256=" + hex.EncodeToString(cert.PubkeySHA256[:]), "PUBKEY_HASH=" + hex.EncodeToString(cert.PubkeySHA256[:]), // backwards compat with pre-0.15.0; not documented "CERT_FILENAME=" + cert.CertPath, "JSON_FILENAME=" + cert.JSONPath, "TEXT_FILENAME=" + cert.TextPath, } if cert.Info.ValidityParseError == nil { env = append(env, "NOT_BEFORE="+cert.Info.Validity.NotBefore.String()) env = append(env, "NOT_BEFORE_UNIXTIME="+fmt.Sprint(cert.Info.Validity.NotBefore.Unix())) env = append(env, "NOT_BEFORE_RFC3339="+cert.Info.Validity.NotBefore.Format(time.RFC3339)) env = append(env, "NOT_AFTER="+cert.Info.Validity.NotAfter.String()) env = append(env, "NOT_AFTER_UNIXTIME="+fmt.Sprint(cert.Info.Validity.NotAfter.Unix())) env = append(env, "NOT_AFTER_RFC3339="+cert.Info.Validity.NotAfter.Format(time.RFC3339)) } else { env = append(env, "VALIDITY_PARSE_ERROR="+cert.Info.ValidityParseError.Error()) } if cert.Info.SubjectParseError == nil { env = append(env, "SUBJECT_DN="+cert.Info.Subject.String()) } else { env = append(env, "SUBJECT_PARSE_ERROR="+cert.Info.SubjectParseError.Error()) } if cert.Info.IssuerParseError == nil { env = append(env, "ISSUER_DN="+cert.Info.Issuer.String()) } else { env = append(env, "ISSUER_PARSE_ERROR="+cert.Info.IssuerParseError.Error()) } if cert.Info.SerialNumberParseError == nil { env = append(env, "SERIAL="+fmt.Sprintf("%x", cert.Info.SerialNumber)) } else { env = append(env, "SERIAL_PARSE_ERROR="+cert.Info.SerialNumberParseError.Error()) } return env } func (cert *discoveredCert) Text() string { // TODO-4: improve the output: include WatchItem, indicate hash algorithm used for fingerprints, ... (look at SSLMate email for inspiration) text := new(strings.Builder) writeField := func(name string, value any) { fmt.Fprintf(text, "\t%13s = %s\n", name, value) } fmt.Fprintf(text, "%x:\n", cert.SHA256) for _, dnsName := range cert.Identifiers.DNSNames { writeField("DNS Name", dnsName) } for _, ipaddr := range cert.Identifiers.IPAddrs { writeField("IP Address", ipaddr) } writeField("Pubkey", hex.EncodeToString(cert.PubkeySHA256[:])) if cert.Info.IssuerParseError == nil { writeField("Issuer", cert.Info.Issuer) } else { writeField("Issuer", fmt.Sprintf("[unable to parse: %s]", cert.Info.IssuerParseError)) } if cert.Info.ValidityParseError == nil { writeField("Not Before", cert.Info.Validity.NotBefore) writeField("Not After", cert.Info.Validity.NotAfter) } else { writeField("Not Before", fmt.Sprintf("[unable to parse: %s]", cert.Info.ValidityParseError)) writeField("Not After", fmt.Sprintf("[unable to parse: %s]", cert.Info.ValidityParseError)) } writeField("Log Entry", fmt.Sprintf("%d @ %s", cert.LogEntry.Index, cert.LogEntry.Log.URL)) writeField("crt.sh", "https://crt.sh/?sha256="+hex.EncodeToString(cert.SHA256[:])) if cert.CertPath != "" { writeField("Filename", cert.CertPath) } return text.String() } func (cert *discoveredCert) EmailSubject() string { return fmt.Sprintf("[certspotter] Certificate Discovered for %s", cert.WatchItem) }