certdataDiffCCADB/certdata/certdata.go (220 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 certdata import ( "bufio" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "errors" "fmt" "io" "math/big" "regexp" "strconv" "strings" "github.com/mozilla/CCADB-Tools/certdataDiffCCADB/utils" ) // Strings that mark the beginning of blocks of text important for parsing certdata.txt. const ( URL = "https://hg.mozilla.org/releases/mozilla-beta/raw-file/tip/security/nss/lib/ckfw/builtins/certdata.txt" StartCertificate = "CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE" // Declaration of start of Certificate object. StartTrust = "CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST" // Declaration of start of a Distrust object. WebDistrust = "CKA_TRUST_SERVER_AUTH CK_TRUST (CKT_NSS_MUST_VERIFY_TRUST|CKT_NSS_NOT_TRUSTED)" WebTrust = "CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR" EmailDistrust = "CKA_TRUST_EMAIL_PROTECTION CK_TRUST (CKT_NSS_MUST_VERIFY_TRUST|CKT_NSS_NOT_TRUSTED)" EmailTrust = "CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR" IssuerPrefix = "CKA_ISSUER MULTILINE_OCTAL" // Declaration of start of a CKA_ISSUER block SerialNumberPrefix = "CKA_SERIAL_NUMBER MULTILINE_OCTAL" // Declaration of start of a CKA_SERIAL_NUMBER block. PEMPrefix = "CKA_VALUE MULTILINE_OCTAL" // Declaration of start a CKA_VALUE (PEM) block. ) var distrustWebRegex *regexp.Regexp var distrustEmailRegex *regexp.Regexp var countryName asn1.ObjectIdentifier var organization asn1.ObjectIdentifier var organizationalUnitName asn1.ObjectIdentifier var commonName asn1.ObjectIdentifier var stateOrProvinceName asn1.ObjectIdentifier var localityName asn1.ObjectIdentifier var email asn1.ObjectIdentifier var serial asn1.ObjectIdentifier func init() { distrustWebRegex = regexp.MustCompile(WebDistrust) distrustEmailRegex = regexp.MustCompile(EmailDistrust) countryName = asn1.ObjectIdentifier{2, 5, 4, 6} organization = asn1.ObjectIdentifier{2, 5, 4, 10} organizationalUnitName = asn1.ObjectIdentifier{2, 5, 4, 11} commonName = asn1.ObjectIdentifier{2, 5, 4, 3} stateOrProvinceName = asn1.ObjectIdentifier{2, 5, 4, 8} localityName = asn1.ObjectIdentifier{2, 5, 4, 7} email = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 1} serial = asn1.ObjectIdentifier{2, 5, 4, 5} } // ParseToNormalizedForm parses the provided certdata.txt into a normalized form // that can be use to easily compare against a CCADB report. func ParseToNormalizedForm(f io.Reader) ([]*utils.Entry, error) { b := bufio.NewReader(f) lineNum := 0 r := make([]*utils.Entry, 0) for l, err := b.ReadString('\n'); err == nil; l, err = b.ReadString('\n') { lineNum++ // A "distrust" object is a bare Trust object that is not preceeded by a Certificate object. if distrust := strings.HasPrefix(l, StartTrust); strings.HasPrefix(l, StartCertificate) || distrust { e, l, err := Extract(b, lineNum, distrust, "certdata") lineNum += l if err != nil { return r, err } r = append(r, e) } } return r, nil } // NewEntry constructs a new utils.Entry from the parsed ASN.1 issuer field, the serial number // as a hex a string, the PEM as a base64 encoded string, the line number where entry started on, // and the absolute path to the file where the entity was extracted from. func NewEntry(i pkix.RDNSequence, s string, pem string, hash string, webTrust, emailTrust bool, ln int, fname string) *utils.Entry { var on string var oun string var cn string // A pkix.RDNSequence is just a type alias of a slice of OIDs, of which the // exact ordering is not guaranteed. As such, we just have to iterate over // the OIDs to figure out what they are. for _, attr := range i { oid := attr[0].Type switch value := attr[0].Value.(string); true { case oid.Equal(organization): on = value case oid.Equal(organizationalUnitName): oun = value case oid.Equal(commonName): cn = value } } return utils.NewEntry(on, oun, cn, s, pem, hash, webTrust, emailTrust, ln, fname) } // Extract extracts the entity from the bufio.Reader, 'b', that starts line number 'start'. // 'distrust' is whether or not the entity is a distrust object. This is necessary since // distrust objects do not have a PEM to parse out. func Extract(b *bufio.Reader, start int, distrust bool, fname string) (*utils.Entry, int, error) { issuerFound := false serialFound := false pemFound := false webTrustFound := false emailTrustFound := false lineNum := 0 var issuer pkix.RDNSequence var serialNum string var pem string var hash string var webTrust bool var emailTrust bool for l, err := b.ReadString('\n'); err == nil; l, err = b.ReadString('\n') { lineNum++ // Putting this break guard here instead of the loop signature makes it easier to count lines. // Distrust objects lack a PEM. Hence (pemFound || distrust). if issuerFound && serialFound && webTrustFound && emailTrustFound && (pemFound || distrust) { break } switch true { case strings.HasPrefix(l, IssuerPrefix): oct, lcount := ExtractMultilineOctal(b) lineNum += lcount issuerFound = true if issuer, err = DecodeIssuer(oct); err != nil { return nil, lineNum, err } case strings.HasPrefix(l, SerialNumberPrefix): oct, lcount := ExtractMultilineOctal(b) lineNum += lcount serialFound = true if serialNum, err = DecodeSerialNumber(oct); err != nil { return nil, lineNum, err } case strings.HasPrefix(l, SerialNumberPrefix): oct, lcount := ExtractMultilineOctal(b) lineNum += lcount serialFound = true if serialNum, err = DecodeSerialNumber(oct); err != nil { return nil, lineNum, err } case !distrust && strings.HasPrefix(l, PEMPrefix): oct, lcount := ExtractMultilineOctal(b) lineNum += lcount pemFound = true if pem, hash, err = DecodeDER(oct); err != nil { return nil, lineNum, err } case strings.HasPrefix(l, WebTrust): webTrust = true webTrustFound = true case distrustWebRegex.MatchString(l): webTrust = false webTrustFound = true case strings.HasPrefix(l, EmailTrust): emailTrust = true emailTrustFound = true case distrustEmailRegex.MatchString(l): emailTrust = false emailTrustFound = true } } if !(issuerFound && serialFound && webTrustFound && emailTrustFound && (pemFound || distrust)) { return nil, lineNum, errors.New("unexpected EOF") } e := NewEntry(issuer, serialNum, pem, hash, webTrust, emailTrust, start, fname) return e, lineNum, nil } // DecodeIssuer parses the CKA_ISSUER MULTILINE_OCTAL field of certdata.txt. func DecodeIssuer(octal string) (pkix.RDNSequence, error) { b, err := otobs(octal) if err != nil { return nil, err } var i pkix.RDNSequence if rest, err := asn1.Unmarshal(b, &i); err != nil { return nil, err } else if len(rest) != 0 { return nil, errors.New("issuer field had trailing data") } return i, nil } // DecodeSerialNumber takes a DER encoded octal string and returns // the base64 encoded serial number. func DecodeSerialNumber(octal string) (string, error) { b, err := otobs(octal) if err != nil { return "", err } s := new(big.Int) if rest, err := asn1.Unmarshal(b, &s); err != nil { return "", err } else if len(rest) != 0 { return "", errors.New("serial number field had trailing data") } // %x fmts to hex return fmt.Sprintf("%x", s), nil } // DecodeDER takes a DER encoded octal string and returns the base64 // encoded certificate as well as its SHA-256 hash. No newlines, BEGIN, // or END fields are present on the decoded string. func DecodeDER(octal string) (string, string, error) { b, err := otobs(octal) if err != nil { return "", "", err } c, err := x509.ParseCertificate(b) if err != nil { return "", "", err } h := sha256.New() h.Write(b) f := FmtFingerprint(fmt.Sprintf("%x", h.Sum(nil))) pem := base64.StdEncoding.EncodeToString(c.Raw) return pem, f, nil } // FmtFingerprint formats a SHA 256 hash with colons. func FmtFingerprint(h string) string { h = strings.ToUpper(h) f := make([]byte, 95) // 64 characters + 31 ':' copy(f[0:2], h[0:2]) ci := 2 for i := 2; i < len(h); i += 2 { f[ci] = ':' copy(f[ci+1:ci+3], h[i:i+2]) ci += 3 } return string(f) } // ExtractMultilineOctal consumes the provided bufio.Reader and returns a string // of '\' delimited octal values and then number of lines consumed to extract // the octal value. func ExtractMultilineOctal(b *bufio.Reader) (string, int) { var oct []string lines := 0 for l, err := b.ReadString('\n'); err == nil; l, err = b.ReadString('\n') { lines++ // Putting this break guard here instead of the loop signature makes it easier to count lines. if strings.HasPrefix(l, "END") { break } oct = append(oct, strings.Trim(l, "\n")) } return strings.Join(oct, ""), lines } // otobs converts a string containing `\` delimited octal values to a byte slice. // An error can occur if a supposed octal value fails to convert to an integer. func otobs(oct string) (result []byte, err error) { var b int64 for _, o := range strings.Split(oct, `\`)[1:] { if b, err = strconv.ParseInt(o, 8, 0); err != nil { return } result = append(result, byte(b)) } return }