// Copyright (C) 2016 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 certspotter import ( "fmt" "time" "os" "os/exec" "bytes" "io" "io/ioutil" "math/big" "path/filepath" "strconv" "strings" "crypto/sha256" "encoding/hex" "encoding/pem" "encoding/json" "software.sslmate.com/src/certspotter/ct" ) func ReadSTHFile (path string) (*ct.SignedTreeHead, error) { content, err := ioutil.ReadFile(path) if err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } var sth ct.SignedTreeHead if err := json.Unmarshal(content, &sth); err != nil { return nil, err } return &sth, nil } func WriteSTHFile (path string, sth *ct.SignedTreeHead) error { sthJson, err := json.MarshalIndent(sth, "", "\t") if err != nil { return err } sthJson = append(sthJson, byte('\n')) return ioutil.WriteFile(path, sthJson, 0666) } func WriteProofFile (path string, proof ct.ConsistencyProof) error { proofJson, err := json.MarshalIndent(proof, "", "\t") if err != nil { return err } proofJson = append(proofJson, byte('\n')) return ioutil.WriteFile(path, proofJson, 0666) } func IsPrecert (entry *ct.LogEntry) bool { return entry.Leaf.TimestampedEntry.EntryType == ct.PrecertLogEntryType } func GetFullChain (entry *ct.LogEntry) [][]byte { certs := make([][]byte, 0, len(entry.Chain) + 1) if entry.Leaf.TimestampedEntry.EntryType == ct.X509LogEntryType { certs = append(certs, entry.Leaf.TimestampedEntry.X509Entry) } for _, cert := range entry.Chain { certs = append(certs, cert) } return certs } func formatSerialNumber (serial *big.Int) string { if serial != nil { return fmt.Sprintf("%x", serial) } else { return "" } } func sha256sum (data []byte) []byte { sum := sha256.Sum256(data) return sum[:] } func sha256hex (data []byte) string { return hex.EncodeToString(sha256sum(data)) } type EntryInfo struct { LogUri string Entry *ct.LogEntry IsPrecert bool FullChain [][]byte // first entry is logged X509 cert or pre-cert CertInfo *CertInfo ParseError error // set iff CertInfo is nil Identifiers *Identifiers IdentifiersParseError error Filename string } type CertInfo struct { TBS *TBSCertificate Subject RDNSequence SubjectParseError error Issuer RDNSequence IssuerParseError error SANs []SubjectAltName SANsParseError error SerialNumber *big.Int SerialNumberParseError error Validity *CertValidity ValidityParseError error IsCA *bool IsCAParseError error } func MakeCertInfoFromTBS (tbs *TBSCertificate) *CertInfo { info := &CertInfo{TBS: tbs} info.Subject, info.SubjectParseError = tbs.ParseSubject() info.Issuer, info.IssuerParseError = tbs.ParseIssuer() info.SANs, info.SANsParseError = tbs.ParseSubjectAltNames() info.SerialNumber, info.SerialNumberParseError = tbs.ParseSerialNumber() info.Validity, info.ValidityParseError = tbs.ParseValidity() info.IsCA, info.IsCAParseError = tbs.ParseBasicConstraints() return info } func MakeCertInfoFromRawTBS (tbsBytes []byte) (*CertInfo, error) { tbs, err := ParseTBSCertificate(tbsBytes) if err != nil { return nil, err } return MakeCertInfoFromTBS(tbs), nil } func MakeCertInfoFromRawCert (certBytes []byte) (*CertInfo, error) { cert, err := ParseCertificate(certBytes) if err != nil { return nil, err } return MakeCertInfoFromRawTBS(cert.GetRawTBSCertificate()) } func MakeCertInfoFromLogEntry (entry *ct.LogEntry) (*CertInfo, error) { switch entry.Leaf.TimestampedEntry.EntryType { case ct.X509LogEntryType: return MakeCertInfoFromRawCert(entry.Leaf.TimestampedEntry.X509Entry) case ct.PrecertLogEntryType: return MakeCertInfoFromRawTBS(entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate) default: return nil, fmt.Errorf("MakeCertInfoFromCTEntry: unknown CT entry type (neither X509 nor precert)") } } func (info *CertInfo) NotBefore () *time.Time { if info.ValidityParseError == nil { return &info.Validity.NotBefore } else { return nil } } func (info *CertInfo) NotAfter () *time.Time { if info.ValidityParseError == nil { return &info.Validity.NotAfter } else { return nil } } func (info *CertInfo) PubkeyHash () string { return sha256hex(info.TBS.GetRawPublicKey()) } func (info *CertInfo) PubkeyHashBytes () []byte { return sha256sum(info.TBS.GetRawPublicKey()) } func (info *CertInfo) Environ () []string { env := make([]string, 0, 10) env = append(env, "PUBKEY_HASH=" + info.PubkeyHash()) if info.SerialNumberParseError != nil { env = append(env, "SERIAL_PARSE_ERROR=" + info.SerialNumberParseError.Error()) } else { env = append(env, "SERIAL=" + formatSerialNumber(info.SerialNumber)) } if info.ValidityParseError != nil { env = append(env, "VALIDITY_PARSE_ERROR=" + info.ValidityParseError.Error()) } else { env = append(env, "NOT_BEFORE=" + info.Validity.NotBefore.String()) env = append(env, "NOT_BEFORE_UNIXTIME=" + strconv.FormatInt(info.Validity.NotBefore.Unix(), 10)) env = append(env, "NOT_AFTER=" + info.Validity.NotAfter.String()) env = append(env, "NOT_AFTER_UNIXTIME=" + strconv.FormatInt(info.Validity.NotAfter.Unix(), 10)) } if info.SubjectParseError != nil { env = append(env, "SUBJECT_PARSE_ERROR=" + info.SubjectParseError.Error()) } else { env = append(env, "SUBJECT_DN=" + info.Subject.String()) } if info.IssuerParseError != nil { env = append(env, "ISSUER_PARSE_ERROR=" + info.IssuerParseError.Error()) } else { env = append(env, "ISSUER_DN=" + info.Issuer.String()) } // TODO: include SANs in environment return env } func (info *EntryInfo) HasParseErrors () bool { return info.ParseError != nil || info.IdentifiersParseError != nil || info.CertInfo.SubjectParseError != nil || info.CertInfo.IssuerParseError != nil || info.CertInfo.SANsParseError != nil || info.CertInfo.SerialNumberParseError != nil || info.CertInfo.ValidityParseError != nil || info.CertInfo.IsCAParseError != nil } func (info *EntryInfo) Fingerprint () string { if len(info.FullChain) > 0 { return sha256hex(info.FullChain[0]) } else { return "" } } func (info *EntryInfo) FingerprintBytes () []byte { if len(info.FullChain) > 0 { return sha256sum(info.FullChain[0]) } else { return []byte{} } } func (info *EntryInfo) typeString () string { if info.IsPrecert { return "precert" } else { return "cert" } } func (info *EntryInfo) typeFriendlyString () string { if info.IsPrecert { return "Pre-certificate" } else { return "Certificate" } } func yesnoString (value bool) string { if value { return "yes" } else { return "no" } } func (info *EntryInfo) Environ () []string { env := []string{ "FINGERPRINT=" + info.Fingerprint(), "CERT_TYPE=" + info.typeString(), "CERT_PARSEABLE=" + yesnoString(info.ParseError == nil), "LOG_URI=" + info.LogUri, "ENTRY_INDEX=" + strconv.FormatInt(info.Entry.Index, 10), } if info.Filename != "" { env = append(env, "CERT_FILENAME=" + info.Filename) } if info.ParseError != nil { env = append(env, "PARSE_ERROR=" + info.ParseError.Error()) } else if info.CertInfo != nil { certEnv := info.CertInfo.Environ() env = append(env, certEnv...) } if info.IdentifiersParseError != nil { env = append(env, "IDENTIFIERS_PARSE_ERROR=" + info.IdentifiersParseError.Error()) } else if info.Identifiers != nil { env = append(env, "DNS_NAMES=" + info.Identifiers.dnsNamesString(",")) env = append(env, "IP_ADDRESSES=" + info.Identifiers.ipAddrsString(",")) } return env } func writeField (out io.Writer, name string, value interface{}, err error) { if err == nil { fmt.Fprintf(out, "\t%13s = %s\n", name, value) } else { fmt.Fprintf(out, "\t%13s = *** UNKNOWN (%s) ***\n", name, err) } } func (info *EntryInfo) Write (out io.Writer) { fingerprint := info.Fingerprint() fmt.Fprintf(out, "%s:\n", fingerprint) if info.IdentifiersParseError != nil { writeField(out, "Identifiers", nil, info.IdentifiersParseError) } else if info.Identifiers != nil { for _, dnsName := range info.Identifiers.DNSNames { writeField(out, "DNS Name", dnsName, nil) } for _, ipaddr := range info.Identifiers.IPAddrs { writeField(out, "IP Address", ipaddr, nil) } } if info.ParseError != nil { writeField(out, "Parse Error", "*** " + info.ParseError.Error() + " ***", nil) } else if info.CertInfo != nil { writeField(out, "Pubkey", info.CertInfo.PubkeyHash(), nil) writeField(out, "Subject", info.CertInfo.Subject, info.CertInfo.SubjectParseError) if info.CertInfo.SANsParseError != nil { writeField(out, "Alt Names", nil, info.CertInfo.SANsParseError) } else { for _, san := range info.CertInfo.SANs { writeField(out, "Alt Name", san.String(), nil) } } writeField(out, "Issuer", info.CertInfo.Issuer, info.CertInfo.IssuerParseError) writeField(out, "Serial", info.CertInfo.SerialNumber, info.CertInfo.SerialNumberParseError) writeField(out, "Not Before", info.CertInfo.NotBefore(), info.CertInfo.ValidityParseError) writeField(out, "Not After", info.CertInfo.NotAfter(), info.CertInfo.ValidityParseError) } writeField(out, "Type", info.typeFriendlyString(), nil) writeField(out, "Log Entry", fmt.Sprintf("%d @ %s", info.Entry.Index, info.LogUri), nil) writeField(out, "crt.sh", "https://crt.sh/?q=" + fingerprint, nil) if info.Filename != "" { writeField(out, "Filename", info.Filename, nil) } } func (info *EntryInfo) InvokeHookScript (command string) error { cmd := exec.Command(command) cmd.Env = os.Environ() infoEnv := info.Environ() cmd.Env = append(cmd.Env, infoEnv...) stderrBuffer := bytes.Buffer{} cmd.Stderr = &stderrBuffer if err := cmd.Run(); err != nil { if _, isExitError := err.(*exec.ExitError); isExitError { fmt.Errorf("Script failed: %s: %s", command, strings.TrimSpace(stderrBuffer.String())) } else { fmt.Errorf("Failed to execute script: %s: %s", command, err) } } return nil } func WriteCertRepository (repoPath string, isPrecert bool, certs [][]byte) (bool, string, error) { if len(certs) == 0 { return false, "", fmt.Errorf("Cannot write an empty certificate chain") } fingerprint := sha256hex(certs[0]) prefixPath := filepath.Join(repoPath, fingerprint[0:2]) var filenameSuffix string if isPrecert { filenameSuffix = ".precert.pem" } else { filenameSuffix = ".cert.pem" } if err := os.Mkdir(prefixPath, 0777); err != nil && !os.IsExist(err) { return false, "", fmt.Errorf("Failed to create prefix directory %s: %s", prefixPath, err) } path := filepath.Join(prefixPath, fingerprint + filenameSuffix) file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666) if err != nil { if os.IsExist(err) { return true, path, nil } else { return false, path, fmt.Errorf("Failed to open %s for writing: %s", path, err) } } for _, cert := range certs { if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: cert}); err != nil { file.Close() return false, path, fmt.Errorf("Error writing to %s: %s", path, err) } } if err := file.Close(); err != nil { return false, path, fmt.Errorf("Error writing to %s: %s", path, err) } return false, path, nil }