certspotter/helpers.go

409 lines
11 KiB
Go

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"
)
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
Filename string
}
type CertInfo struct {
TBS *TBSCertificate
CommonNames []string
CommonNamesParseError error
DNSNames []string
DNSNamesParseError error
Subject RDNSequence
SubjectParseError error
Issuer RDNSequence
IssuerParseError error
SerialNumber *big.Int
SerialNumberParseError error
Validity *CertValidity
ValidityParseError error
Constraints *BasicConstraints
ConstraintsParseError error
}
func MakeCertInfoFromTBS (tbs *TBSCertificate) *CertInfo {
info := &CertInfo{TBS: tbs}
info.CommonNames, info.CommonNamesParseError = tbs.ParseCommonNames()
info.DNSNames, info.DNSNamesParseError = tbs.ParseDNSNames()
info.Subject, info.SubjectParseError = tbs.ParseSubject()
info.Issuer, info.IssuerParseError = tbs.ParseIssuer()
info.SerialNumber, info.SerialNumberParseError = tbs.ParseSerialNumber()
info.Validity, info.ValidityParseError = tbs.ParseValidity()
info.Constraints, info.ConstraintsParseError = 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) commonNamesString () string {
if info.CommonNamesParseError == nil {
return strings.Join(info.CommonNames, ", ")
} else {
return ""
}
}
func (info *CertInfo) dnsNamesString () string {
if info.DNSNamesParseError == nil {
return strings.Join(info.DNSNames, ", ")
} else {
return ""
}
}
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.CommonNamesParseError != nil {
env = append(env, "COMMON_NAMES_PARSE_ERROR=" + info.CommonNamesParseError.Error())
} else {
env = append(env, "COMMON_NAMES=" + strings.Join(info.CommonNames, ","))
}
if info.DNSNamesParseError != nil {
env = append(env, "DNS_NAMES_PARSE_ERROR=" + info.DNSNamesParseError.Error())
} else {
env = append(env, "DNS_NAMES=" + strings.Join(info.DNSNames, ","))
}
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())
}
return env
}
func (info *EntryInfo) HasParseErrors () bool {
return info.ParseError != nil ||
info.CertInfo.CommonNamesParseError != nil ||
info.CertInfo.DNSNamesParseError != nil ||
info.CertInfo.SubjectParseError != nil ||
info.CertInfo.IssuerParseError != nil ||
info.CertInfo.SerialNumberParseError != nil ||
info.CertInfo.ValidityParseError != nil ||
info.CertInfo.ConstraintsParseError != 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 {
certEnv := info.CertInfo.Environ()
env = append(env, certEnv...)
} else {
env = append(env, "PARSE_ERROR=" + info.ParseError.Error())
}
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.ParseError != nil {
writeField(out, "Parse Error", "*** " + info.ParseError.Error() + " ***", nil)
} else {
writeField(out, "Common Name", info.CertInfo.commonNamesString(), info.CertInfo.CommonNamesParseError)
writeField(out, "DNS Names", info.CertInfo.dnsNamesString(), info.CertInfo.DNSNamesParseError)
writeField(out, "Pubkey", info.CertInfo.PubkeyHash(), nil)
writeField(out, "Subject", info.CertInfo.Subject, info.CertInfo.SubjectParseError)
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
}