blob: 1af3e0ae46fe1cd4e4fdbba5f4bd361c82a4fe9b [file] [log] [blame]
// 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
}