go/rootprogram/issuers.go (334 lines of code) (raw):

package rootprogram import ( "context" "crypto/sha256" "encoding/base64" "encoding/csv" "encoding/json" "encoding/pem" "fmt" "io" "io/ioutil" "net/url" "os" "strings" "sync" "time" "github.com/golang/glog" "github.com/google/certificate-transparency-go/x509" "github.com/mozilla/crlite/go" "github.com/mozilla/crlite/go/downloader" ) const ( kMozCCADBReport = "https://ccadb.my.salesforce-sites.com/mozilla/MozillaIntermediateCertsCSVReport" ) type issuerCert struct { cert *x509.Certificate subjectDN string pemInfo string } type IssuerData struct { certs []issuerCert } type EnrolledIssuer struct { UniqueID string `json:"uniqueID"` PubKeyHash string `json:"pubKeyHash"` Subject string `json:"subject"` Pem string `json:"pem"` } type MozIssuers struct { issuerMap map[string]IssuerData CrlMap types.IssuerCrlMap mutex *sync.Mutex DiskPath string ReportUrl string modTime time.Time } func NewMozillaIssuers() *MozIssuers { return &MozIssuers{ issuerMap: make(map[string]IssuerData, 0), CrlMap: make(types.IssuerCrlMap, 0), mutex: &sync.Mutex{}, DiskPath: fmt.Sprintf("%s/mozilla_issuers.csv", os.TempDir()), ReportUrl: kMozCCADBReport, } } type verifier struct { } func (v *verifier) IsValid(path string) error { mi := NewMozillaIssuers() return mi.LoadFromDisk(path) } type loggingAuditor struct{} func (ta *loggingAuditor) FailedDownload(issuer downloader.DownloadIdentifier, crlUrl *url.URL, dlTracer *downloader.DownloadTracer, err error) { glog.Warningf("Failed download of %s: %s", crlUrl.String(), err) } func (ta *loggingAuditor) FailedVerifyUrl(issuer downloader.DownloadIdentifier, crlUrl *url.URL, dlTracer *downloader.DownloadTracer, err error) { glog.Warningf("Failed verify of %s: %s", crlUrl.String(), err) } func (ta *loggingAuditor) FailedVerifyPath(issuer downloader.DownloadIdentifier, crlUrl *url.URL, crlPath string, err error) { glog.Warningf("Failed verify of %s (local: %s): %s", crlUrl.String(), crlPath, err) } type identifier struct{} func (i *identifier) ID() string { return "Mozilla Issuers" } func (mi *MozIssuers) Load() error { ctx := context.Background() dataUrl, err := url.Parse(mi.ReportUrl) if err != nil { glog.Fatalf("Couldn't parse CCADB URL of %s: %s", mi.ReportUrl, err) return err } isAcceptable, err := downloader.DownloadAndVerifyFileSync(ctx, &verifier{}, &loggingAuditor{}, &identifier{}, *dataUrl, mi.DiskPath, 3, 300*time.Second) if !isAcceptable { return err } if err != nil { glog.Warningf("Error encountered loading CCADB data, but able to proceed with previous data. Error: %s", err) } return mi.LoadFromDisk(mi.DiskPath) } func (mi *MozIssuers) LoadFromDisk(aPath string) error { fd, err := os.Open(aPath) if err != nil { return err } defer fd.Close() fi, err := os.Stat(aPath) if err != nil { return err } mi.modTime = fi.ModTime() return mi.parseCCADB(fd) } func (mi *MozIssuers) DatasetAge() time.Duration { if mi.modTime.IsZero() { return 0 } return time.Since(mi.modTime) } func (mi *MozIssuers) GetIssuers() []types.Issuer { mi.mutex.Lock() defer mi.mutex.Unlock() issuers := make([]types.Issuer, len(mi.issuerMap)) i := 0 for _, value := range mi.issuerMap { cert := value.certs[0].cert issuers[i] = types.NewIssuer(cert) i++ } return issuers } func normalizePem(input string) string { // Some consumers of the file produced by `SaveIssuersList` mistakenly // assume that the PEM encoding of a certificate is unique. This causes // some problems as the CCADB report often includes a certificate with // an unusual PEM presentation one day and a different presentation // another. (Usually a 65 character line that is later reflowed to // width 64.) As a work-around, we'll normalize to the PEM format // produced by the go standard library modulo the trailing newline. We // omit the trailing newline to minimize differences with the entries // in the CCADB report at the time of writing. // var pemBuf strings.Builder derBytes, rest := pem.Decode([]byte(input)) if len(rest) != 0 { glog.Warningf("Ignored %d bytes of trailing data while normalizing this PEM: %s", len(rest), input) } pem.Encode(&pemBuf, derBytes) output := pemBuf.String() output = strings.TrimRight(output, "\n") return output } func (mi *MozIssuers) SaveIssuersList(filePath string) error { mi.mutex.Lock() defer mi.mutex.Unlock() certCount := 0 issuers := make([]EnrolledIssuer, 0, len(mi.issuerMap)) for _, val := range mi.issuerMap { for _, cert := range val.certs { pubKeyHash := sha256.Sum256(cert.cert.RawSubjectPublicKeyInfo) uniqueID := sha256.Sum256(append(cert.cert.RawSubject, cert.cert.RawSubjectPublicKeyInfo...)) issuers = append(issuers, EnrolledIssuer{ UniqueID: base64.URLEncoding.EncodeToString(uniqueID[:]), PubKeyHash: base64.URLEncoding.EncodeToString(pubKeyHash[:]), Subject: cert.subjectDN, Pem: normalizePem(cert.pemInfo), }) certCount++ } } glog.Infof("Saving %d issuers and %d certs", len(mi.issuerMap), certCount) fd, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { glog.Errorf("Error opening enrolled issuer %s: %s", filePath, err) return err } enc := json.NewEncoder(fd) if err := enc.Encode(issuers); err != nil { glog.Errorf("Error marshaling enrolled issuer %s: %s", filePath, err) } if err = fd.Close(); err != nil { glog.Errorf("Error storing enrolled issuer %s: %s", filePath, err) } return err } func (mi *MozIssuers) LoadEnrolledIssuers(filePath string) error { bytes, err := ioutil.ReadFile(filePath) if err != nil { return err } list := make([]EnrolledIssuer, 0) err = json.Unmarshal(bytes, &list) if err != nil { return err } for _, ei := range list { cert, err := decodeCertificateFromPem(ei.Pem) if err != nil { return err } mi.InsertIssuerFromCertAndPem(cert, ei.Pem, nil) } return nil } func (mi *MozIssuers) IsIssuerInProgram(aIssuer types.Issuer) bool { _, ok := mi.issuerMap[aIssuer.ID()] return ok } func (mi *MozIssuers) GetCertificateForIssuer(aIssuer types.Issuer) (*x509.Certificate, error) { mi.mutex.Lock() defer mi.mutex.Unlock() entry, ok := mi.issuerMap[aIssuer.ID()] if !ok { return nil, fmt.Errorf("Unknown issuer: %s", aIssuer.ID()) } return entry.certs[0].cert, nil } func (mi *MozIssuers) GetSubjectForIssuer(aIssuer types.Issuer) (string, error) { mi.mutex.Lock() defer mi.mutex.Unlock() entry, ok := mi.issuerMap[aIssuer.ID()] if !ok { return "", fmt.Errorf("Unknown issuer: %s", aIssuer.ID()) } return entry.certs[0].subjectDN, nil } func decodeCertificateFromPem(aPem string) (*x509.Certificate, error) { block, rest := pem.Decode([]byte(aPem)) if block == nil { return nil, fmt.Errorf("Not a valid PEM") } if len(rest) != 0 { return nil, fmt.Errorf("Extra PEM data") } return x509.ParseCertificate(block.Bytes) } func decodeCertificateFromRow(aColMap map[string]int, aRow []string, aLineNum int) (*x509.Certificate, error) { p := strings.Trim(aRow[aColMap["PEM"]], "'") cert, err := decodeCertificateFromPem(p) if err != nil { return nil, fmt.Errorf("%s at line %d", err, aLineNum) } return cert, nil } func decodeCrlsFromRow(aColMap map[string]int, aRow []string, aLineNum int) ([]string, error) { crls := []string{} fullCrlStr := aRow[aColMap["Full CRL Issued By This CA"]] fullCrlStr = strings.TrimSpace(fullCrlStr) if fullCrlStr != "" { fullCrlUrl, err := url.Parse(fullCrlStr) if err != nil { glog.Warningf("decodeCrlsFromRow: Line %d: Could not parse %q as URL: %v", aLineNum, fullCrlStr, err) } else if fullCrlUrl.Scheme != "http" && fullCrlUrl.Scheme != "https" { glog.Warningf("decodeCrlsFromRow: Line %d: Unknown URL scheme in %q", aLineNum, fullCrlUrl.String()) } else { crls = append(crls, fullCrlUrl.String()) } } partCrlJson := aRow[aColMap["JSON Array of Partitioned CRLs"]] partCrlJson = strings.Trim(strings.TrimSpace(partCrlJson), "[]") partCrls := strings.Split(partCrlJson, ",") for _, crl := range partCrls { crl = strings.TrimSpace(crl) if crl == "" { continue } crlUrl, err := url.Parse(crl) if err != nil { glog.Warningf("decodeCrlsFromRow: Line %d: Could not parse %q as URL: %v", aLineNum, crl, err) } else if crlUrl.Scheme != "http" && crlUrl.Scheme != "https" { glog.Warningf("decodeCrlsFromRow: Line %d: Unknown URL scheme in %q", aLineNum, crlUrl.String()) } else { crls = append(crls, crlUrl.String()) } } return crls, nil } func (mi *MozIssuers) InsertIssuerFromCertAndPem(aCert *x509.Certificate, aPem string, crls []string) types.Issuer { issuer := types.NewIssuer(aCert) ic := issuerCert{ cert: aCert, subjectDN: aCert.Subject.String(), pemInfo: aPem, } crlSet, exists := mi.CrlMap[issuer.ID()] if !exists { crlSet = make(map[string]bool, 0) } for _, crl := range crls { crlSet[crl] = true } mi.CrlMap[issuer.ID()] = crlSet v, exists := mi.issuerMap[issuer.ID()] if exists { glog.V(1).Infof("[%s] Duplicate issuer ID: %v with %v", issuer.ID(), v, aCert.Subject.String()) v.certs = append(v.certs, ic) mi.issuerMap[issuer.ID()] = v return issuer } mi.issuerMap[issuer.ID()] = IssuerData{ certs: []issuerCert{ic}, } return issuer } func (mi *MozIssuers) NewTestIssuerFromSubjectString(aSub string) types.Issuer { issuer := types.NewIssuerFromString(aSub) ic := issuerCert{ subjectDN: aSub, } mi.issuerMap[issuer.ID()] = IssuerData{ certs: []issuerCert{ic}, } return issuer } func (mi *MozIssuers) parseCCADB(aStream io.Reader) error { mi.mutex.Lock() defer mi.mutex.Unlock() reader := csv.NewReader(aStream) columnMap := make(map[string]int) columns, err := reader.Read() if err != nil { return err } for index, attr := range columns { columnMap[attr] = index } lineNum := 1 for { row, err := reader.Read() if err == io.EOF { break } if err != nil { return err } lineNum += 1 cert, err := decodeCertificateFromRow(columnMap, row, lineNum) if err != nil { return err } crls, err := decodeCrlsFromRow(columnMap, row, lineNum) if err != nil { return err } _ = mi.InsertIssuerFromCertAndPem(cert, strings.Trim(row[columnMap["PEM"]], "'"), crls) lineNum += strings.Count(strings.Join(row, ""), "\n") } return nil }