402 lines
11 KiB
Go
402 lines
11 KiB
Go
// 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, "Issuer", info.CertInfo.Issuer, info.CertInfo.IssuerParseError)
|
|
writeField(out, "Not Before", info.CertInfo.NotBefore(), info.CertInfo.ValidityParseError)
|
|
writeField(out, "Not After", info.CertInfo.NotAfter(), info.CertInfo.ValidityParseError)
|
|
}
|
|
writeField(out, "Log Entry", fmt.Sprintf("%d @ %s (%s)", info.Entry.Index, info.LogUri, info.typeFriendlyString()), 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
|
|
}
|