package ctwatch import ( "fmt" "time" "os" "os/exec" "bytes" "io" "io/ioutil" "math/big" "path/filepath" "strconv" "strings" "crypto/sha256" "encoding/hex" "encoding/pem" "encoding/json" "src.agwa.name/ctwatch/ct" "src.agwa.name/ctwatch/ct/x509" "src.agwa.name/ctwatch/ct/x509/pkix" ) 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 EntryDNSNames (entry *ct.LogEntry) ([]string, error) { switch entry.Leaf.TimestampedEntry.EntryType { case ct.X509LogEntryType: return ExtractDNSNames(entry.Leaf.TimestampedEntry.X509Entry) case ct.PrecertLogEntryType: return ExtractDNSNamesFromTBS(entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate) } panic("EntryDNSNames: entry is neither precert nor x509") } func ParseEntryCertificate (entry *ct.LogEntry) (*x509.Certificate, error) { if entry.Leaf.TimestampedEntry.EntryType == ct.PrecertLogEntryType { return x509.ParseTBSCertificate(entry.Leaf.TimestampedEntry.PrecertEntry.TBSCertificate) } else if entry.Leaf.TimestampedEntry.EntryType == ct.X509LogEntryType { return x509.ParseCertificate(entry.Leaf.TimestampedEntry.X509Entry) } else { panic("ParseEntryCertificate: entry is neither precert nor x509") } } func appendDnArray (buf *bytes.Buffer, code string, values []string) { for _, value := range values { if buf.Len() != 0 { buf.WriteString(", ") } buf.WriteString(code) buf.WriteString("=") buf.WriteString(value) } } func appendDnValue (buf *bytes.Buffer, code string, value string) { if value != "" { appendDnArray(buf, code, []string{value}) } } func formatDN (name pkix.Name) (string) { // C=US, ST=UT, L=Salt Lake City, O=The USERTRUST Network, OU=http://www.usertrust.com, CN=UTN-USERFirst-Hardware var buf bytes.Buffer appendDnArray(&buf, "C", name.Country) appendDnArray(&buf, "ST", name.Province) appendDnArray(&buf, "L", name.Locality) appendDnArray(&buf, "O", name.Organization) appendDnArray(&buf, "OU", name.OrganizationalUnit) appendDnValue(&buf, "CN", name.CommonName) return buf.String() } func allDNSNames (cert *x509.Certificate) []string { dnsNames := []string{} if cert.Subject.CommonName != "" { dnsNames = append(dnsNames, cert.Subject.CommonName) } for _, dnsName := range cert.DNSNames { if dnsName != cert.Subject.CommonName { dnsNames = append(dnsNames, dnsName) } } return dnsNames } func formatSerial (serial *big.Int) string { if serial != nil { return fmt.Sprintf("%x", serial) } else { return "" } } func sha256hex (data []byte) string { sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) } func GetRawCert (entry *ct.LogEntry) []byte { switch entry.Leaf.TimestampedEntry.EntryType { case ct.X509LogEntryType: return entry.Leaf.TimestampedEntry.X509Entry case ct.PrecertLogEntryType: return entry.Chain[0] } panic("GetRawCert: entry is neither precert nor x509") } func IsPrecert (entry *ct.LogEntry) bool { switch entry.Leaf.TimestampedEntry.EntryType { case ct.PrecertLogEntryType: return true case ct.X509LogEntryType: return false } panic("IsPrecert: entry is neither precert nor x509") } type EntryInfo struct { LogUri string Entry *ct.LogEntry ParsedCert *x509.Certificate ParseError error CertInfo CertInfo Filename string } type CertInfo struct { DnsNames []string SubjectDn string IssuerDn string Serial string PubkeyHash string NotBefore *time.Time NotAfter *time.Time } func MakeCertInfo (cert *x509.Certificate) CertInfo { return CertInfo { DnsNames: allDNSNames(cert), SubjectDn: formatDN(cert.Subject), IssuerDn: formatDN(cert.Issuer), Serial: formatSerial(cert.SerialNumber), PubkeyHash: sha256hex(cert.RawSubjectPublicKeyInfo), NotBefore: &cert.NotBefore, NotAfter: &cert.NotAfter, } } func (info *CertInfo) dnsNamesFriendlyString () string { if info.DnsNames != nil { return strings.Join(info.DnsNames, ", ") } else { return "*** UNKNOWN ***" } } func (info *CertInfo) Environ () []string { var env []string if info.DnsNames != nil { env = append(env, "DNS_NAMES=" + strings.Join(info.DnsNames, ",")) } if info.SubjectDn != "" { env = append(env, "SUBJECT_DN=" + info.SubjectDn) } if info.IssuerDn != "" { env = append(env, "ISSUER_DN=" + info.IssuerDn) } if info.Serial != "" { env = append(env, "SERIAL=" + info.Serial) } if info.PubkeyHash != "" { env = append(env, "PUBKEY_HASH=" + info.PubkeyHash) } if info.NotBefore != nil { env = append(env, "NOT_BEFORE=" + strconv.FormatInt(info.NotBefore.Unix(), 10)) } if info.NotAfter != nil { env = append(env, "NOT_AFTER=" + strconv.FormatInt(info.NotAfter.Unix(), 10)) } return env } func (info *EntryInfo) GetRawCert () []byte { return GetRawCert(info.Entry) } func (info *EntryInfo) Fingerprint () string { return sha256hex(info.GetRawCert()) } func (info *EntryInfo) IsPrecert () bool { return IsPrecert(info.Entry) } 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.ParsedCert != 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()) } certEnv := info.CertInfo.Environ() env = append(env, certEnv...) return env } func (info *EntryInfo) Write (out io.Writer) { fingerprint := info.Fingerprint() fmt.Fprintf(out, "%s:\n", fingerprint) if info.ParseError != nil { if info.ParsedCert != nil { fmt.Fprintf(out, "\tParse Warning = *** %s ***\n", info.ParseError) } else { fmt.Fprintf(out, "\t Parse Error = *** %s ***\n", info.ParseError) } } fmt.Fprintf(out, "\t DNS Names = %s\n", info.CertInfo.dnsNamesFriendlyString()) if info.CertInfo.PubkeyHash != "" { fmt.Fprintf(out, "\t Pubkey = %s\n", info.CertInfo.PubkeyHash) } if info.CertInfo.SubjectDn != "" { fmt.Fprintf(out, "\t Subject = %s\n", info.CertInfo.SubjectDn) } if info.CertInfo.IssuerDn != "" { fmt.Fprintf(out, "\t Issuer = %s\n", info.CertInfo.IssuerDn) } if info.CertInfo.Serial != "" { fmt.Fprintf(out, "\t Serial = %s\n", info.CertInfo.Serial) } if info.CertInfo.NotBefore != nil { fmt.Fprintf(out, "\t Not Before = %s\n", *info.CertInfo.NotBefore) } if info.CertInfo.NotAfter != nil { fmt.Fprintf(out, "\t Not After = %s\n", *info.CertInfo.NotAfter) } fmt.Fprintf(out, "\t Type = %s\n", info.typeFriendlyString()) fmt.Fprintf(out, "\t Log Entry = %d @ %s\n", info.Entry.Index, info.LogUri) fmt.Fprintf(out, "\t crt.sh = https://crt.sh/?q=%s\n", fingerprint) if info.Filename != "" { fmt.Fprintf(out, "\t Filename = %s\n", info.Filename) } } 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, entry *ct.LogEntry) (bool, string, error) { fingerprint := sha256hex(GetRawCert(entry)) prefixPath := filepath.Join(repoPath, fingerprint[0:2]) var filenameSuffix string if entry.Leaf.TimestampedEntry.EntryType == ct.PrecertLogEntryType { filenameSuffix = ".precert.pem" } else if entry.Leaf.TimestampedEntry.EntryType == ct.X509LogEntryType { 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) } } if entry.Leaf.TimestampedEntry.EntryType == ct.X509LogEntryType { if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: entry.Leaf.TimestampedEntry.X509Entry}); err != nil { file.Close() return false, path, fmt.Errorf("Error writing to %s: %s", path, err) } } for _, chainCert := range entry.Chain { if err := pem.Encode(file, &pem.Block{Type: "CERTIFICATE", Bytes: chainCert}); 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 }