entryMaker/oneCRL/oneCRL.go (561 lines of code) (raw):

/* 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/. */ package oneCRL import ( "bufio" "bytes" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "github.com/mozilla/OneCRL-Tools/entryMaker/bugs" "github.com/mozilla/OneCRL-Tools/entryMaker/config" "io/ioutil" "log" "net/http" "os" "strconv" "strings" "time" ) const IssuerPrefix string = "issuer: " const SerialPrefix string = "serial: " // TODO: this looks unecessary - maybe remove type OneCRLUpdate struct { Data Record `json:"data"` } type Record struct { Id string `json:"id,omitempty"` IssuerName string `json:"issuerName,omitempty"` SerialNumber string `json:"serialNumber,omitempty"` Subject string `json:"subject,omitempty"` PubKeyHash string `json:"pubKeyHash,omitempty"` Enabled bool `json:"enabled"` Details struct { Who string `json:"who"` Created string `json:"created"` Bug string `json:"bug"` Name string `json:"name"` Why string `json:"why"` } `json:"details"` } func (record Record) EqualsRecord(otherRecord Record) bool { return record.IssuerName == otherRecord.IssuerName && record.SerialNumber == otherRecord.SerialNumber && record.Subject == otherRecord.Subject && record.PubKeyHash == otherRecord.PubKeyHash } type Records struct { Data []Record `json:"data"` } // the subset of stuff we actually care about from Kinto metadata type KintoMetadata struct { User struct { Principals []string `json:"principals"` Id string `json:"id"` } `json:"user"` } func StringFromRecord(record Record) string { if "" != record.Subject { return stringFromSubjectPubKeyHash(record.Subject, record.PubKeyHash) } return StringFromIssuerSerial(record.IssuerName, record.SerialNumber) } func stringFromSubjectPubKeyHash(subject string, pubKeyHash string) string { return fmt.Sprintf("subject: %s pubKeyHash: %s", subject, pubKeyHash) } func StringFromIssuerSerial(issuer string, serial string) string { return fmt.Sprintf("issuer: %s serial: %s", issuer, serial) } func getDataFromURL(url string, conf *config.OneCRLConfig) ([]byte, error) { req, err := http.NewRequest("GET", url, nil) if len(conf.KintoToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", conf.KintoToken)) } else if len(conf.KintoUser) > 0 { req.SetBasicAuth(conf.KintoUser, conf.KintoPassword) } client := &http.Client{} resp, err := client.Do(req) if nil != err { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) } func FetchExistingRevocations(url string) (*Records, error) { conf := config.GetConfig() if len(url) == 0 { return nil, errors.New("No URL was specified") } if "yes" == conf.OneCRLVerbose { fmt.Printf("Got URL data\n") } res := new(Records) if data, err := getDataFromURL(url, conf); nil != err { return nil, errors.New(fmt.Sprintf("problem loading existing data from URL %s", err)) } else { if err := json.Unmarshal(data, res); nil != err { return nil, err } else { return res, nil } } } func ByteArrayEquals(a []byte, b []byte) bool { if len(a) != len(b) { return false } for i, v := range a { if v != b[i] { return false } } return true } func DNToRFC4514(name string) (string, error) { rawDN, _ := base64.StdEncoding.DecodeString(name) rdns := new(pkix.RDNSequence) _, err := asn1.Unmarshal(rawDN, rdns) return RFC4514ish(*rdns), err } func hexify(arr []byte, separate bool, upperCase bool) string { var encoded bytes.Buffer for i := 0; i < len(arr); i++ { encoded.WriteString(strings.ToUpper(hex.EncodeToString(arr[i : i+1]))) if i < len(arr)-1 && separate { encoded.WriteString(":") } } retval := encoded.String() if !upperCase { retval = strings.ToLower(retval) } return retval } func SerialToString(encoded string, separate bool, upper bool) (string, error) { rawSerial, err := base64.StdEncoding.DecodeString(encoded) return hexify(rawSerial, separate, upper), err } func NamesDataMatches(name1 []byte, name2 []byte) bool { // Go's asn.1 marshalling support does not maintain original encodings. // Because if this, if the data are the same other than the encodings then // although bytewise comparisons on the original data failed, we can assume // that encoding differences will go away when we marshal back from // pkix.RDNSequence back to actual asn.1 data. // ensure our names decode to pkix.RDNSequences rdns1 := new(pkix.RDNSequence) _, errUnmarshal1 := asn1.Unmarshal(name1, rdns1) if nil != errUnmarshal1 { return false } rdns2 := new(pkix.RDNSequence) _, errUnmarshal2 := asn1.Unmarshal(name2, rdns2) if nil != errUnmarshal2 { return false } marshalled1, marshall1err := asn1.Marshal(*rdns1) if nil != marshall1err { return false } marshalled2, marshall2err := asn1.Marshal(*rdns2) if nil != marshall2err { return false } return ByteArrayEquals(marshalled1, marshalled2) } func RFC4514ish(rdns pkix.RDNSequence) string { retval := "" for _, rdn := range rdns { if len(rdn) == 0 { continue } atv := rdn[0] value, ok := atv.Value.(string) if !ok { continue } t := atv.Type tStr := "" if len(t) == 4 && t[0] == 2 && t[1] == 5 && t[2] == 4 { switch t[3] { case 3: tStr = "CN" case 7: tStr = "L" case 8: tStr = "ST" case 10: tStr = "O" case 11: tStr = "OU" case 6: tStr = "C" case 9: tStr = "STREET" } } if len(t) == 7 && t[0] == 1 && t[1] == 2 && t[2] == 840 && t[3] == 113549 && t[4] == 1 && t[5] == 9 && t[6] == 1 { tStr = "emailAddress" } sep := "" if len(retval) > 0 { sep = ", " } // quote values that contain a comma if strings.Contains(value, ",") { value = "\"\"" + value + "\"\"" } retval = retval + sep + tStr + "=" + value } return retval } type OneCRLLoader interface { LoadRecord(record Record) } // TODO: fix loading functions to get data from a reader func LoadJSONFromURL(url string, loader OneCRLLoader) error { var err error res := new(Records) r, err := http.Get(url) if err != nil { return err } defer r.Body.Close() err = json.NewDecoder(r.Body).Decode(res) if nil != err { return err } for idx := range res.Data { loader.LoadRecord(res.Data[idx]) } return nil } func LoadRevocationsTxtFromFile(filename string, loader OneCRLLoader) error { var ( err error ) file, err := os.Open(filename) if err != nil { log.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) var dn = "" for scanner.Scan() { // process line line := scanner.Text() // Ignore comments if 0 == strings.Index(line, "#") { continue } if 0 == strings.Index(line, " ") { if len(dn) == 0 { log.Fatal("A serial number with no issuer is not valid. Exiting.") } record := Record{IssuerName: dn, SerialNumber: strings.Trim(line, " ")} loader.LoadRecord(record) continue } if 0 == strings.Index(line, "\t") { if len(dn) == 0 { log.Fatal("A public key hash with no subject is not valid. Exiting.") } record := Record{Subject: dn, PubKeyHash: strings.Trim(line, "\t")} loader.LoadRecord(record) continue } dn = line } if err = scanner.Err(); err != nil { log.Fatal(err) } return nil } func LoadRevocationsFromBug(filename string, loader OneCRLLoader) error { conf := config.GetConfig() file, err := os.Open(filename) if err != nil { log.Fatal(err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { // process line line := scanner.Text() // parse the issuer and serial lines from the bug data issuerIndex := strings.Index(line, IssuerPrefix) serialIndex := strings.Index(line, SerialPrefix) issuer := line[issuerIndex+len(IssuerPrefix) : serialIndex-1] serial := line[serialIndex+len(SerialPrefix) : len(line)] if "yes" == conf.OneCRLVerbose { fmt.Printf("Loading revocation. issuer: \"%s\", serial: \"%s\"\n", issuer, serial) } record := Record{IssuerName: issuer, SerialNumber: serial} loader.LoadRecord(record) } if err = scanner.Err(); err != nil { log.Fatal(err) } return nil } func checkResponseStatus(resp *http.Response, message string) error { if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { bodyBytes, _ := ioutil.ReadAll(resp.Body) bodyString := string(bodyBytes) fmt.Printf("Error: server response is %s\n", bodyString) return errors.New(fmt.Sprintf("%s: %d", message, resp.StatusCode)) } return nil } func AddKintoObject(url string, obj interface{}) error { conf := config.GetConfig() marshalled, _ := json.Marshal(obj) if conf.Preview != "yes" { if "yes" == conf.OneCRLVerbose { fmt.Printf("Will POST to \"%s\" with \"%s\"\n", url+"/records", marshalled) } req, err := http.NewRequest("POST", url+"/records", bytes.NewBuffer(marshalled)) if len(conf.KintoToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", conf.KintoToken)) } else if len(conf.KintoUser) > 0 { req.SetBasicAuth(conf.KintoUser, conf.KintoPassword) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if nil != err { panic(err) } err = checkResponseStatus(resp, "There was a problem adding a record") defer resp.Body.Close() if nil != err { return err } } else { fmt.Printf("Would POST to \"%s\" with \"%s\"\n", url+"/records", marshalled) } return nil } func checkKintoAuth(collectionUrl string) error { conf := config.GetConfig() kintoBase := strings.SplitAfter(collectionUrl, "/v1/")[0] req, err := http.NewRequest("GET", kintoBase, nil) if len(conf.KintoToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", conf.KintoToken)) } else if len(conf.KintoUser) > 0 { req.SetBasicAuth(conf.KintoUser, conf.KintoPassword) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if nil != err { return err } err = checkResponseStatus(resp, "There was a problem checking auth status") if nil != err { return err } res := new(KintoMetadata) err = json.NewDecoder(resp.Body).Decode(res) if nil != err { return err } if "" == res.User.Id { return errors.New("Cannot perform Kinto operations; user is not authenticated") } fmt.Printf("authenticated as user %s\n", res.User.Id) defer resp.Body.Close() return nil } func AddEntries(records *Records, existing *Records, createBug bool, comment string) error { conf := config.GetConfig() issuerMap := make(map[string][]string) bugStyle := "" bugNum := -1 shouldWrite := conf.Preview != "yes" && len(records.Data) > 0 now := time.Now() nowString := now.Format("2006-01-02T15:04:05Z") // Check that we're correctly authenticated to Kinto if shouldWrite { err := checkKintoAuth(conf.KintoCollectionURL) if nil != err { return err } } // File a bugzilla bug - so we've got a bug URL to add to the kinto entries if shouldWrite && !conf.SkipBugzilla { bug := bugs.Bug{} bug.ApiKey = conf.BugzillaAPIKey blocks, err := strconv.Atoi(conf.BugzillaBlockee) if len(conf.BugzillaBlockee) != 0 { if nil == err { bug.Blocks = append(bug.Blocks, blocks) } } bug.Product = conf.BugProduct bug.Component = conf.BugComponent bug.Version = conf.BugVersion bug.Summary = fmt.Sprintf("CCADB entries generated %s", nowString) bug.Description = conf.BugDescription bug.Type = "task" bugNum, err = bugs.CreateBug(bug, conf) if err != nil { panic(err) } fmt.Printf("Added bug: %d\n", bugNum) } for _, record := range records.Data { // TODO: We don't need to build an issuer map if we're not outputting // entries directly. If we *do* need to do this, the functionality for // making revocations.txt style data should live in oneCRL.go if issuers, ok := issuerMap[record.IssuerName]; ok { issuerMap[record.IssuerName] = append(issuers, record.SerialNumber) } else { issuerMap[record.IssuerName] = []string{record.SerialNumber} } if record.Details.Bug == "" { record.Details.Bug = fmt.Sprintf("%s/show_bug.cgi?id=%d", conf.BugzillaBase, bugNum) } if record.Details.Created == "" { record.Details.Created = nowString } bugStyle = bugStyle + StringFromRecord(record) + "\n" // TODO: Batch these, don't send single requests if conf.Preview != "yes" { if "yes" == conf.OneCRLVerbose { fmt.Printf("record data is %s\n", StringFromRecord(record)) } } } // Generate the data to attach to the bug bugStyleData := []byte(bugStyle) rTxt := new(RevocationsTxtData) for _, record := range existing.Data { rTxt.LoadRecord(record) } for _, record := range records.Data { rTxt.LoadRecord(record) } revocationsTxtString := rTxt.ToRevocationsTxtString() revocationsTxtData := []byte(revocationsTxtString) if shouldWrite { // Update Kinto records for _, record := range records.Data { update := new(OneCRLUpdate) update.Data = record // Upload the created entry to Kinto err := AddKintoObject(conf.KintoCollectionURL, update) if nil != err { panic(err) } } // Request review reviewJSON := "{\"data\": {\"status\": \"to-review\"}}" req, err := http.NewRequest("PATCH", conf.KintoCollectionURL, bytes.NewBuffer([]byte(reviewJSON))) if len(conf.KintoToken) > 0 { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", conf.KintoToken)) } else if len(conf.KintoUser) > 0 { req.SetBasicAuth(conf.KintoUser, conf.KintoPassword) } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if "yes" == conf.OneCRLVerbose { fmt.Printf("requested review - status code is %d\n", resp.StatusCode) } defer resp.Body.Close() if err != nil { panic(err) } err = checkResponseStatus(resp, "There was a problem with requesting review") if nil != err { return err } // upload the created entries to bugzilla attachments := make([]bugs.Attachment, 2) encodedBugStyleData := base64.StdEncoding.EncodeToString(bugStyleData) attachments[0] = bugs.Attachment{} attachments[0].FileName = "BugData.txt" attachments[0].Summary = "Intermediates to be revoked" attachments[0].ContentType = "text/plain" attachments[0].Comment = "Revocations data for new records" attachments[0].ApiKey = conf.BugzillaAPIKey attachments[0].Data = encodedBugStyleData attachments[0].Flags = make([]bugs.AttachmentFlag, 0, 1) encodedRevocationsTxtData := base64.StdEncoding.EncodeToString(revocationsTxtData) attachments[1] = bugs.Attachment{} attachments[1].FileName = "revocations.txt" attachments[1].Summary = "existing and new revocations in the form of a revocations.txt file" attachments[1].ContentType = "text/plain" attachments[1].Comment = "Revocations data for new and existing records" attachments[1].ApiKey = conf.BugzillaAPIKey attachments[1].Data = encodedRevocationsTxtData attachments[1].Flags = make([]bugs.AttachmentFlag, 0, 1) // create flags for the reviewers for _, reviewer := range strings.Split(conf.BugzillaReviewers, ",") { trimmedReviewer := strings.Trim(reviewer, " ") if len(trimmedReviewer) > 0 { flag := bugs.AttachmentFlag{} flag.Name = "data-review" flag.Status = "?" flag.Requestee = trimmedReviewer flag.New = true attachments[0].Flags = append(attachments[0].Flags, flag) attachments[1].Flags = append(attachments[1].Flags, flag) } } if !conf.SkipBugzilla { if err = bugs.AttachToBug(bugNum, conf.BugzillaAPIKey, attachments, conf); err != nil { panic(err) } if err = bugs.AddCommentToBug(bugNum, conf, comment); err != nil { panic(err) } } } else { // If we aren't supposed to write, save locally bugDataFile, err := ioutil.TempFile("", "BugData.txt") if err != nil { panic(err) } if _, err = bugDataFile.Write(bugStyleData); err != nil { panic(err) } fmt.Printf("BugData.txt written to %s\n", bugDataFile.Name()) revocationsFile, err := ioutil.TempFile("", "revocations.txt") if err != nil { panic(err) } if _, err = revocationsFile.Write(revocationsTxtData); err != nil { panic(err) } fmt.Printf("revocations.txt written to %s\n", revocationsFile.Name()) } return nil } type RevocationsTxtData struct { byIssuerSerialNumber map[string][]string bySubjectPubKeyHash map[string][]string } func (r *RevocationsTxtData) LoadRecord(record Record) { // if there's no issuer name, assume we're revoking by Subject / PubKeyHash // otherwise it's issuer / serial if 0 == len(record.IssuerName) { if nil == r.bySubjectPubKeyHash { r.bySubjectPubKeyHash = make(map[string][]string) } if nil == r.bySubjectPubKeyHash[record.Subject] { pubKeyHashes := make([]string, 1) pubKeyHashes[0] = record.PubKeyHash r.bySubjectPubKeyHash[record.Subject] = pubKeyHashes } else { r.bySubjectPubKeyHash[record.Subject] = append(r.bySubjectPubKeyHash[record.Subject], record.PubKeyHash) } } else { if nil == r.byIssuerSerialNumber { r.byIssuerSerialNumber = make(map[string][]string) } if nil == r.byIssuerSerialNumber[record.IssuerName] { serials := make([]string, 1) serials[0] = record.SerialNumber r.byIssuerSerialNumber[record.IssuerName] = serials } else { r.byIssuerSerialNumber[record.IssuerName] = append(r.byIssuerSerialNumber[record.IssuerName], record.SerialNumber) } } } func (r *RevocationsTxtData) ToRevocationsTxtString() string { RevocationsTxtString := "" for issuer, serials := range r.byIssuerSerialNumber { RevocationsTxtString = fmt.Sprintf("%s%s\n", RevocationsTxtString, issuer) for _, serial := range serials { RevocationsTxtString = fmt.Sprintf("%s %s\n", RevocationsTxtString, serial) } } for subject, pubKeyHashes := range r.bySubjectPubKeyHash { RevocationsTxtString = fmt.Sprintf("%s%s\n", RevocationsTxtString, subject) for _, pubKeyHash := range pubKeyHashes { RevocationsTxtString = fmt.Sprintf("%s\t%s\n", RevocationsTxtString, pubKeyHash) } } return RevocationsTxtString }