// Copyright 2023 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package nss provides functionality for parsing NSS certdata.txt // formatted certificate lists and extracting serverAuth roots. Most // users should not use this package themselves, and should instead // rely on the golang.org/x/crypto/x509roots/fallback package which // calls x509.SetFallbackRoots on a pre-parsed set of roots. package nss import ( "bufio" "bytes" "crypto/sha1" "crypto/x509" "errors" "fmt" "io" "strconv" "strings" "time" ) // Constraint is a constraint to be applied to a certificate or // certificate chain. type Constraint interface { Kind() Kind } // Kind is the constraint kind, using the NSS enumeration. type Kind int const ( CKA_NSS_SERVER_DISTRUST_AFTER Kind = iota ) // DistrustAfter is a Constraint that indicates a certificate has a // CKA_NSS_SERVER_DISTRUST_AFTER constraint. This constraint defines a date // after which any certificate issued which is rooted by the constrained // certificate should be distrusted. type DistrustAfter time.Time func (DistrustAfter) Kind() Kind { return CKA_NSS_SERVER_DISTRUST_AFTER } // A Certificate represents a single trusted serverAuth certificate in the NSS // certdata.txt list and any constraints that should be applied to chains // rooted by it. type Certificate struct { // Certificate is the parsed certificate X509 *x509.Certificate // Constraints contains a list of additional constraints that should be // applied to any certificates that chain to Certificate. If there are // any unknown constraints in the slice, Certificate should not be // trusted. Constraints []Constraint } func parseMulitLineOctal(s *bufio.Scanner) ([]byte, error) { buf := bytes.NewBuffer(nil) for s.Scan() { if s.Text() == "END" { break } b, err := strconv.Unquote(fmt.Sprintf("\"%s\"", s.Text())) if err != nil { return nil, err } buf.Write([]byte(b)) } return buf.Bytes(), nil } type certObj struct { c *x509.Certificate DistrustAfter *time.Time } func parseCertClass(s *bufio.Scanner) ([sha1.Size]byte, *certObj, error) { var h [sha1.Size]byte co := &certObj{} for s.Scan() { l := s.Text() if l == "" { // assume an empty newline indicates the end of a block break } if strings.HasPrefix(l, "CKA_VALUE") { b, err := parseMulitLineOctal(s) if err != nil { return h, nil, err } co.c, err = x509.ParseCertificate(b) if err != nil { return h, nil, err } h = sha1.Sum(b) } else if strings.HasPrefix(l, "CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_FALSE") { // we don't want it return h, nil, nil } else if l == "CKA_NSS_SERVER_DISTRUST_AFTER MULTILINE_OCTAL" { dateStr, err := parseMulitLineOctal(s) if err != nil { return h, nil, err } t, err := time.Parse("060102150405Z0700", string(dateStr)) if err != nil { return h, nil, err } co.DistrustAfter = &t } } if co.c == nil { return h, nil, errors.New("malformed CKO_CERTIFICATE object") } return h, co, nil } type trustObj struct { trusted bool } func parseTrustClass(s *bufio.Scanner) ([sha1.Size]byte, *trustObj, error) { var h [sha1.Size]byte to := &trustObj{trusted: false} // default to untrusted for s.Scan() { l := s.Text() if l == "" { // assume an empty newline indicates the end of a block break } if l == "CKA_CERT_SHA1_HASH MULTILINE_OCTAL" { hash, err := parseMulitLineOctal(s) if err != nil { return h, nil, err } copy(h[:], hash) } else if l == "CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR" { // we only care about server auth to.trusted = true } } return h, to, nil } // Parse parses a NSS certdata.txt formatted file, returning only // trusted serverAuth roots, as well as any additional constraints. This parser // is very opinionated, only returning roots that are currently trusted for // serverAuth. As such roots returned by this package should only be used for // making trust decisions about serverAuth certificates, as the trust status for // other uses is not considered. Using the roots returned by this package for // trust decisions should be done carefully. // // Some roots returned by the parser may include additional constraints // (currently only DistrustAfter) which need to be considered when verifying // certificates which chain to them. // // Parse is not intended to be a general purpose parser for certdata.txt. func Parse(r io.Reader) ([]*Certificate, error) { // certdata.txt is a rather strange format. It is essentially a list of // textual PKCS#11 objects, delimited by empty lines. There are two main // types of objects, certificates (CKO_CERTIFICATE) and trust definitions // (CKO_NSS_TRUST). These objects appear to alternate, but this ordering is // not defined anywhere, and should probably not be relied on. A single root // certificate requires both the certificate object and the trust definition // object in order to be properly understood. // // The list contains not just serverAuth certificates, so we need to be // careful to only extract certificates which have the serverAuth trust bit // set. Similarly there are a number of trust related bool fields that // appear to _always_ be CKA_TRUE, but it seems unsafe to assume this is the // case, so we should always double check. // // Since we only really care about a couple of fields, this parser throws // away a lot of information, essentially just consuming CKA_CLASS objects // and looking for the individual fields we care about. We could write a // siginificantly more complex parser, which handles the entire format, but // it feels like that would be over engineered for the little information // that we really care about. scanner := bufio.NewScanner(r) type nssEntry struct { cert *certObj trust *trustObj } entries := map[[sha1.Size]byte]*nssEntry{} for scanner.Scan() { // scan until we hit CKA_CLASS if !strings.HasPrefix(scanner.Text(), "CKA_CLASS") { continue } f := strings.Fields(scanner.Text()) if len(f) != 3 { return nil, errors.New("malformed CKA_CLASS") } switch f[2] { case "CKO_CERTIFICATE": h, co, err := parseCertClass(scanner) if err != nil { return nil, err } if co != nil { e, ok := entries[h] if !ok { e = &nssEntry{} entries[h] = e } e.cert = co } case "CKO_NSS_TRUST": h, to, err := parseTrustClass(scanner) if err != nil { return nil, err } if to != nil { e, ok := entries[h] if !ok { e = &nssEntry{} entries[h] = e } e.trust = to } } } if err := scanner.Err(); err != nil { return nil, err } var certs []*Certificate for h, e := range entries { if e.cert == nil && e.trust != nil { // We may skip some certificates which are distrusted due to mozilla // policy (CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_FALSE), which means // we might get entries that appear to have a trust object, but no // certificate. We can just continue on here. continue } else if e.cert != nil && e.trust == nil { return nil, fmt.Errorf("missing trust object for certificate with SHA1 hash: %x", h) } if !e.trust.trusted { continue } nssCert := &Certificate{X509: e.cert.c} if e.cert.DistrustAfter != nil { nssCert.Constraints = append(nssCert.Constraints, DistrustAfter(*e.cert.DistrustAfter)) } certs = append(certs, nssCert) } return certs, nil }