blob: 61cf1b7b1f95894047a829b0ead298c559316cda [file] [log] [blame]
// Copyright 2019 The Chromium OS 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 security
import (
"bufio"
"context"
"crypto/sha1"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"syscall"
"chromiumos/tast/common/testexec"
"chromiumos/tast/errors"
"chromiumos/tast/testing"
)
func init() {
testing.AddTest(&testing.Test{
Func: RootCA,
Desc: "Ensures that the built-in root CAs match a baseline",
Contacts: []string{
"jorgelo@chromium.org", // Security team
"ejcaruso@chromium.org", // Tast port author
"chromeos-security@google.com",
},
Data: []string{rootCABaselinePath},
Attr: []string{"group:mainline"},
})
}
const (
rootCABaselinePath = "root_ca_baseline.json"
)
func RootCA(ctx context.Context, s *testing.State) {
getNSSCerts := func() (certs map[string]string, err error) {
nssLibs, err := filepath.Glob("/usr/lib*/libnssckbi.so")
if err != nil {
return nil, err
}
if len(nssLibs) == 0 {
return nil, nil
} else if len(nssLibs) > 1 {
return nil, errors.Errorf("found multiple copies of libnssckbi.so: %v", nssLibs)
}
dir, err := ioutil.TempDir("", "tast.security.RootCA.")
if err != nil {
return nil, err
}
defer os.RemoveAll(dir)
// Create new empty cert DB.
if err := testexec.CommandContext(ctx, "certutil",
"-N", "-d", dir, "-f", "--empty-password").Run(testexec.DumpLogOnError); err != nil {
return nil, err
}
// Add the certs found in the compiled NSS shlib to a new module in the DB.
if err := testexec.CommandContext(ctx, "modutil", "-add", "testroots", "-libfile", nssLibs[0],
"-dbdir", dir, "-force").Run(testexec.DumpLogOnError); err != nil {
return nil, err
}
if output, err := testexec.CommandContext(ctx, "modutil", "-list",
"-dbdir", dir).Output(testexec.DumpLogOnError); err != nil {
return nil, err
} else if !strings.Contains(string(output), "2. testroots") {
return nil, errors.New("testroots PKCS#11 module did not appear in the db")
}
// Dump out the list of root certs.
child := testexec.CommandContext(ctx, "certutil", "-L", "-d", dir, "-h", "all")
output, err := child.Output(testexec.DumpLogOnError)
if err != nil {
return nil, err
}
// Parse a string and return the (first) group match for each matching line.
extractByRegexp := func(text, re string) []string {
var matches []string
for _, match := range regexp.MustCompile(re).FindAllStringSubmatch(text, -1) {
matches = append(matches, match[1])
}
return matches
}
certMap := make(map[string]string)
for _, cert := range extractByRegexp(string(output), `Builtin Object Token:(.+?)\s+C,.?,.?`) {
child = testexec.CommandContext(ctx, "certutil", "-L", "-d", dir, "-n",
fmt.Sprintf("Builtin Object Token:%s", cert))
output, err = child.Output()
if err != nil {
s.Log("Could not find certificate")
continue
}
for _, fp := range extractByRegexp(string(output), `Fingerprint \(SHA1\):\n\s+(\b[:\w]+)\b`) {
certMap[string(fp)] = cert
}
}
return certMap, nil
}
openSSLCertGlob := "/etc/ssl/certs/" + strings.Repeat("[0-9a-f]", 8) + ".*"
getOpenSSLCerts := func() (certs map[string]string, err error) {
getCert := func(path string) (c *x509.Certificate, err error) {
certPem, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
pemBlock, _ := pem.Decode(certPem)
if pemBlock == nil {
return nil, errors.Errorf("couldn't decode PEM format of certificate %v", path)
}
return x509.ParseCertificate(pemBlock.Bytes)
}
certPaths, err := filepath.Glob(openSSLCertGlob)
if err != nil {
return nil, err
}
certMap := make(map[string]string)
for _, certPath := range certPaths {
cert, err := getCert(certPath)
if err != nil {
return nil, err
}
rawFingerprint := sha1.Sum(cert.Raw)
explodedFingerprint := make([]string, len(rawFingerprint))
for i, b := range rawFingerprint {
explodedFingerprint[i] = fmt.Sprintf("%02X", b)
}
fingerprint := strings.Join(explodedFingerprint, ":")
var issuer string
if cert.Issuer.CommonName != "" {
issuer = cert.Issuer.CommonName
} else if cert.Issuer.Organization != nil && len(cert.Issuer.Organization) > 0 {
issuer = cert.Issuer.Organization[0]
} else {
return nil, errors.Errorf("couldn't find issuer string for %v", fingerprint)
}
certMap[fingerprint] = issuer
}
return certMap, nil
}
// The baseline is an object with three fields: "nss", "openssl", and "both".
// Each is itself an object mapping fingerprints to issuer strings. The "both" map
// includes certificates that should be found in both the NSS and OpenSSL cert lists.
parseBaseline := func() (nssCerts, openSSLCerts map[string]string) {
baselineFile, err := os.Open(s.DataPath(rootCABaselinePath))
if err != nil {
s.Fatal("Failed to open baseline: ", err)
}
defer baselineFile.Close()
type Baseline struct {
NSS map[string]string `json:"nss"`
OpenSSL map[string]string `json:"openssl"`
Both map[string]string `json:"both"`
}
var certs Baseline
d := json.NewDecoder(bufio.NewReader(baselineFile))
d.DisallowUnknownFields()
if err := d.Decode(&certs); err != nil {
s.Fatal("Couldn't parse certs baseline: ", err)
}
nssMap := make(map[string]string)
openSSLMap := make(map[string]string)
for fingerprint, issuer := range certs.NSS {
nssMap[fingerprint] = issuer
}
for fingerprint, issuer := range certs.OpenSSL {
openSSLMap[fingerprint] = issuer
}
for fingerprint, issuer := range certs.Both {
nssMap[fingerprint] = issuer
openSSLMap[fingerprint] = issuer
}
return nssMap, openSSLMap
}
compareCerts := func(certStoreName string, expected, found map[string]string) {
// missing is the set difference |expect| - |found|
missing := make(map[string]string)
for fingerprint, issuer := range expected {
if _, ok := found[fingerprint]; !ok {
missing[fingerprint] = issuer
}
}
// unexpected is the set difference |found| - |expected|
unexpected := make(map[string]string)
for fingerprint, issuer := range found {
if _, ok := expected[fingerprint]; !ok {
unexpected[fingerprint] = issuer
}
}
// Ideally, missing and unexpected should be empty. But CA
// root certs are periodically updated.
// Print both missing and unexpected in json, so we can update
// the baseline accordingly.
// Missing certs can be removed from the baseline while
// unexpected can be added, once we have found the corresponding
// official updates, e.g. tracked in the Mozilla's bugzilla.
if len(missing) > 0 {
s.Errorf("In cert store %q, missing %v expected certs", certStoreName, len(missing))
jsonBytes, _ := json.Marshal(missing)
s.Log("missing certs: ", string(jsonBytes))
}
if len(unexpected) > 0 {
s.Errorf("In cert store %q, found %v unexpected certs", certStoreName, len(unexpected))
jsonBytes, _ := json.Marshal(unexpected)
s.Log("unexpcted certs: ", string(jsonBytes))
}
}
nssExpected, openSSLExpected := parseBaseline()
s.Logf("Expecting %v NSS cert(s) and %v OpenSSL cert(s)", len(nssExpected), len(openSSLExpected))
nssFound, err := getNSSCerts()
if err != nil {
s.Fatal("Failed to get NSS certs: ", err)
}
s.Logf("Found %v NSS cert(s)", len(nssFound))
compareCerts("nss", nssExpected, nssFound)
openSSLFound, err := getOpenSSLCerts()
if err != nil {
s.Fatal("Failed to get OpenSSL certs: ", err)
}
s.Logf("Found %v OpenSSL cert(s)", len(openSSLFound))
compareCerts("openssl", openSSLExpected, openSSLFound)
// Regression test for crbug.com/202944
certPaths, err := filepath.Glob(openSSLCertGlob)
if err != nil {
s.Fatal("Failed to glob OpenSSL certs to check perms: ", err)
}
for _, certPath := range certPaths {
stat, err := os.Stat(certPath)
if err != nil {
s.Errorf("Failed to stat OpenSSL cert %v to check perms: %v", certPath, err)
continue
}
st := stat.Sys().(*syscall.Stat_t)
if st.Uid != 0 || stat.Mode().Perm() != 0644 {
s.Errorf("Bad permissions on %v: uid %v, mode %s", certPath, st.Uid, stat.Mode())
}
}
}