Merge pull request #440 from docker/diogo-cli-adding-delegations

delegation command for notary-cli
This commit is contained in:
Ying Li
2016-01-19 13:54:56 -08:00
15 changed files with 889 additions and 46 deletions

212
cmd/notary/delegations.go Normal file
View File

@@ -0,0 +1,212 @@
package main
import (
"fmt"
"io/ioutil"
"github.com/docker/notary"
notaryclient "github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var cmdDelegationTemplate = usageTemplate{
Use: "delegation",
Short: "Operates on delegations.",
Long: `Operations on TUF delegations.`,
}
var cmdDelegationListTemplate = usageTemplate{
Use: "list [ GUN ]",
Short: "Lists delegations for the Global Unique Name.",
Long: "Lists all delegations known to notary for a specific Global Unique Name.",
}
var cmdDelegationRemoveTemplate = usageTemplate{
Use: "remove [ GUN ] [ Role ] <KeyID 1> ...",
Short: "Remove KeyID(s) from the specified Role delegation.",
Long: "Remove KeyID(s) from the specified Role delegation in a specific Global Unique Name.",
}
var cmdDelegationAddTemplate = usageTemplate{
Use: "add [ GUN ] [ Role ] <PEM file path 1> ...",
Short: "Add a keys to delegation using the provided public key certificate PEMs.",
Long: "Add a keys to delegation using the provided public key certificate PEMs in a specific Global Unique Name.",
}
var paths []string
var removeAll, removeYes bool
type delegationCommander struct {
// these need to be set
configGetter func() *viper.Viper
retriever passphrase.Retriever
}
func (d *delegationCommander) GetCommand() *cobra.Command {
cmd := cmdDelegationTemplate.ToCommand(nil)
cmd.AddCommand(cmdDelegationListTemplate.ToCommand(d.delegationsList))
cmdRemDelg := cmdDelegationRemoveTemplate.ToCommand(d.delegationRemove)
cmdRemDelg.Flags().StringSliceVar(&paths, "paths", nil, "List of paths to remove")
cmdRemDelg.Flags().BoolVarP(&removeYes, "yes", "y", false, "Answer yes to the removal question (no confirmation)")
cmd.AddCommand(cmdRemDelg)
cmdAddDelg := cmdDelegationAddTemplate.ToCommand(d.delegationAdd)
cmdAddDelg.Flags().StringSliceVar(&paths, "paths", nil, "List of paths to add")
cmd.AddCommand(cmdAddDelg)
return cmd
}
// delegationsList lists all the delegations for a particular GUN
func (d *delegationCommander) delegationsList(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf(
"Please provide a Global Unique Name as an argument to list")
}
config := d.configGetter()
gun := args[0]
// initialize repo with transport to get latest state of the world before listing delegations
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), getTransport(config, gun, true), retriever)
if err != nil {
return err
}
delegationRoles, err := nRepo.GetDelegationRoles()
if err != nil {
return fmt.Errorf("Error retrieving delegation roles for repository %s: %v", gun, err)
}
cmd.Println("")
prettyPrintRoles(delegationRoles, cmd.Out())
cmd.Println("")
return nil
}
// delegationRemove removes a public key from a specific role in a GUN
func (d *delegationCommander) delegationRemove(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with optional keyIDs and/or a list of paths to remove")
}
config := d.configGetter()
gun := args[0]
role := args[1]
// If we're only given the gun and the role, attempt to remove all data for this delegation
if len(args) == 2 && paths == nil {
removeAll = true
}
keyIDs := []string{}
// Change nil paths to empty slice for TUF
if paths == nil {
paths = []string{}
}
if len(args) > 2 {
keyIDs = args[2:]
}
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, retriever)
if err != nil {
return err
}
if removeAll {
cmd.Println("\nAre you sure you want to remove all data for this delegation? (yes/no)")
// Ask for confirmation before force removing delegation
if !removeYes {
confirmed := askConfirm()
if !confirmed {
fatalf("Aborting action.")
}
} else {
cmd.Println("Confirmed `yes` from flag")
}
}
// Remove the delegation from the repository
err = nRepo.RemoveDelegation(role, keyIDs, paths, removeAll)
if err != nil {
return fmt.Errorf("failed to remove delegation: %v", err)
}
cmd.Println("")
if removeAll {
cmd.Printf("Forced removal (including all keys and paths) of delegation role %s to repository \"%s\" staged for next publish.\n", role, gun)
}
cmd.Printf(
"Removal of delegation role %s with keys %s and paths %s, to repository \"%s\" staged for next publish.\n",
role, keyIDs, paths, gun)
cmd.Println("")
return nil
}
// delegationAdd creates a new delegation by adding a public key from a certificate to a specific role in a GUN
func (d *delegationCommander) delegationAdd(cmd *cobra.Command, args []string) error {
if len(args) < 2 || len(args) < 3 && paths == nil {
return fmt.Errorf("must specify the Global Unique Name and the role of the delegation along with the public key certificate paths and/or a list of paths to add")
}
config := d.configGetter()
gun := args[0]
role := args[1]
pubKeys := []data.PublicKey{}
if len(args) > 2 {
pubKeyPaths := args[2:]
for _, pubKeyPath := range pubKeyPaths {
// Read public key bytes from PEM file
pubKeyBytes, err := ioutil.ReadFile(pubKeyPath)
if err != nil {
return fmt.Errorf("unable to read public key from file: %s", pubKeyPath)
}
// Parse PEM bytes into type PublicKey
pubKey, err := trustmanager.ParsePEMPublicKey(pubKeyBytes)
if err != nil {
return fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", pubKeyPath, err)
}
pubKeys = append(pubKeys, pubKey)
}
}
// no online operations are performed by add so the transport argument
// should be nil
nRepo, err := notaryclient.NewNotaryRepository(config.GetString("trust_dir"), gun, getRemoteTrustServer(config), nil, retriever)
if err != nil {
return err
}
// Add the delegation to the repository
// Sets threshold to 1 since we only added one key - thresholds are not currently fully supported, though
// one can use additional client-side validation to check for signatures from a quorum of varying delegation roles
err = nRepo.AddDelegation(role, notary.MinThreshold, pubKeys, paths)
if err != nil {
return fmt.Errorf("failed to create delegation: %v", err)
}
// Make keyID slice for better CLI print
pubKeyIDs := []string{}
for _, pubKey := range pubKeys {
pubKeyIDs = append(pubKeyIDs, pubKey.ID())
}
cmd.Println("")
cmd.Printf(
"Addition of delegation role %s with keys %s and paths %s, to repository \"%s\" staged for next publish.\n",
role, pubKeyIDs, paths, gun)
cmd.Println("")
return nil
}

View File

@@ -0,0 +1,139 @@
package main
import (
"crypto/rand"
"crypto/x509"
"github.com/docker/notary/cryptoservice"
"github.com/docker/notary/trustmanager"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"io/ioutil"
"os"
"testing"
"time"
)
var testTrustDir = "trust_dir"
func setup() *delegationCommander {
return &delegationCommander{
configGetter: func() *viper.Viper {
mainViper := viper.New()
mainViper.Set("trust_dir", testTrustDir)
return mainViper
},
retriever: nil,
}
}
func TestAddInvalidDelegationName(t *testing.T) {
// Cleanup after test
defer os.RemoveAll(testTrustDir)
// Setup certificate
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
assert.NoError(t, err)
cert, _, err := generateValidTestCert()
_, err = tempFile.Write(trustmanager.CertToPEM(cert))
assert.NoError(t, err)
tempFile.Close()
defer os.Remove(tempFile.Name())
// Setup commander
commander := setup()
// Should error due to invalid delegation name (should be prefixed by "targets/")
err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "INVALID_NAME", tempFile.Name()})
assert.Error(t, err)
}
func TestAddInvalidDelegationCert(t *testing.T) {
// Cleanup after test
defer os.RemoveAll(testTrustDir)
// Setup certificate
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
assert.NoError(t, err)
cert, _, err := generateExpiredTestCert()
_, err = tempFile.Write(trustmanager.CertToPEM(cert))
assert.NoError(t, err)
tempFile.Close()
defer os.Remove(tempFile.Name())
// Setup commander
commander := setup()
// Should error due to expired cert
err = commander.delegationAdd(commander.GetCommand(), []string{"gun", "targets/delegation", tempFile.Name(), "--paths", "path"})
assert.Error(t, err)
}
func TestRemoveInvalidDelegationName(t *testing.T) {
// Cleanup after test
defer os.RemoveAll(testTrustDir)
// Setup commander
commander := setup()
// Should error due to invalid delegation name (should be prefixed by "targets/")
err := commander.delegationRemove(commander.GetCommand(), []string{"gun", "INVALID_NAME", "fake_key_id1", "fake_key_id2"})
assert.Error(t, err)
}
func TestAddInvalidNumArgs(t *testing.T) {
// Setup commander
commander := setup()
// Should error due to invalid number of args (2 instead of 3)
err := commander.delegationAdd(commander.GetCommand(), []string{"not", "enough"})
assert.Error(t, err)
}
func TestListInvalidNumArgs(t *testing.T) {
// Setup commander
commander := setup()
// Should error due to invalid number of args (0 instead of 1)
err := commander.delegationsList(commander.GetCommand(), []string{})
assert.Error(t, err)
}
func TestRemoveInvalidNumArgs(t *testing.T) {
// Setup commander
commander := setup()
// Should error due to invalid number of args (1 instead of 2)
err := commander.delegationRemove(commander.GetCommand(), []string{"notenough"})
assert.Error(t, err)
}
func generateValidTestCert() (*x509.Certificate, string, error) {
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
if err != nil {
return nil, "", err
}
keyID := privKey.ID()
startTime := time.Now()
endTime := startTime.AddDate(10, 0, 0)
cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
if err != nil {
return nil, "", err
}
return cert, keyID, nil
}
func generateExpiredTestCert() (*x509.Certificate, string, error) {
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
if err != nil {
return nil, "", err
}
keyID := privKey.ID()
// Set to Unix time 0 start time, valid for one more day
startTime := time.Unix(0, 0)
endTime := startTime.AddDate(0, 0, 1)
cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
if err != nil {
return nil, "", err
}
return cert, keyID, nil
}

View File

@@ -145,6 +145,223 @@ func TestClientTufInteraction(t *testing.T) {
assert.False(t, strings.Contains(string(output), target))
}
// Initialize repo and test delegations commands by adding, listing, and removing delegations
func TestClientDelegationsInteraction(t *testing.T) {
setUp(t)
tempDir := tempDirWithConfig(t, "{}")
defer os.RemoveAll(tempDir)
server := setupServer()
defer server.Close()
// Setup certificate
tempFile, err := ioutil.TempFile("/tmp", "pemfile")
assert.NoError(t, err)
privKey, err := trustmanager.GenerateECDSAKey(rand.Reader)
startTime := time.Now()
endTime := startTime.AddDate(10, 0, 0)
cert, err := cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
assert.NoError(t, err)
_, err = tempFile.Write(trustmanager.CertToPEM(cert))
assert.NoError(t, err)
tempFile.Close()
defer os.Remove(tempFile.Name())
rawPubBytes, _ := ioutil.ReadFile(tempFile.Name())
parsedPubKey, _ := trustmanager.ParsePEMPublicKey(rawPubBytes)
keyID := parsedPubKey.ID()
var output string
// -- tests --
// init repo
_, err = runCommand(t, tempDir, "-s", server.URL, "init", "gun")
assert.NoError(t, err)
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - none yet
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "No such roles published in this repository.")
// add new valid delegation with single new cert
output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name())
assert.NoError(t, err)
assert.Contains(t, output, "Addition of delegation role")
// check status - see delegation
output, err = runCommand(t, tempDir, "status", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "Unpublished changes for gun")
// list delegations - none yet because still unpublished
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "No such roles published in this repository.")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// check status - no changelist
output, err = runCommand(t, tempDir, "status", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "No unpublished changes for gun")
// list delegations - we should see our added delegation
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "targets/delegation")
// Setup another certificate
tempFile2, err := ioutil.TempFile("/tmp", "pemfile2")
assert.NoError(t, err)
privKey, err = trustmanager.GenerateECDSAKey(rand.Reader)
startTime = time.Now()
endTime = startTime.AddDate(10, 0, 0)
cert, err = cryptoservice.GenerateCertificate(privKey, "gun", startTime, endTime)
assert.NoError(t, err)
_, err = tempFile2.Write(trustmanager.CertToPEM(cert))
assert.NoError(t, err)
assert.NoError(t, err)
tempFile2.Close()
defer os.Remove(tempFile2.Name())
rawPubBytes2, _ := ioutil.ReadFile(tempFile2.Name())
parsedPubKey2, _ := trustmanager.ParsePEMPublicKey(rawPubBytes2)
keyID2 := parsedPubKey2.ID()
// add to the delegation by specifying the same role, this time add a path
output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile2.Name(), "--paths", "path")
assert.NoError(t, err)
assert.Contains(t, output, "Addition of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see two keys
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, ",")
assert.Contains(t, output, "path")
// remove the delegation's first key
output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID)
assert.NoError(t, err)
assert.Contains(t, output, "Removal of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see the delegation but with only the second key
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.NotContains(t, output, ",")
// remove the delegation's second key
output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", keyID2)
assert.NoError(t, err)
assert.Contains(t, output, "Removal of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see no delegations
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "No such roles published in this repository.")
// add delegation with multiple certs and multiple paths
output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", tempFile.Name(), tempFile2.Name(), "--paths", "path1,path2")
assert.NoError(t, err)
assert.Contains(t, output, "Addition of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see two keys
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, ",")
assert.Contains(t, output, "path1,path2")
// add delegation with multiple certs and multiple paths
output, err = runCommand(t, tempDir, "delegation", "add", "gun", "targets/delegation", "--paths", "path3")
assert.NoError(t, err)
assert.Contains(t, output, "Addition of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see two keys
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, ",")
assert.Contains(t, output, "path1,path2,path3")
// just remove two paths from this delegation
output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path2,path3")
assert.NoError(t, err)
assert.Contains(t, output, "Removal of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see the same two keys, and only path1
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, ",")
assert.Contains(t, output, "path1")
assert.NotContains(t, output, "path2")
assert.NotContains(t, output, "path3")
// remove the remaining path, should not remove the delegation entirely
output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "--paths", "path1")
assert.NoError(t, err)
assert.Contains(t, output, "Removal of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see the same two keys, and no paths
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, ",")
assert.NotContains(t, output, "path1")
assert.NotContains(t, output, "path2")
assert.NotContains(t, output, "path3")
// remove by force to delete the delegation entirely
output, err = runCommand(t, tempDir, "delegation", "remove", "gun", "targets/delegation", "-y")
assert.NoError(t, err)
assert.Contains(t, output, "Removal of delegation role")
// publish repo
_, err = runCommand(t, tempDir, "-s", server.URL, "publish", "gun")
assert.NoError(t, err)
// list delegations - we should see no delegations
output, err = runCommand(t, tempDir, "-s", server.URL, "delegation", "list", "gun")
assert.NoError(t, err)
assert.Contains(t, output, "No such roles published in this repository.")
}
// Splits a string into lines, and returns any lines that are not empty (
// striped of whitespace)
func splitLines(chunk string) []string {

View File

@@ -119,7 +119,13 @@ func setupCommand(notaryCmd *cobra.Command) {
retriever: retriever,
}
cmdDelegationGenerator := &delegationCommander{
configGetter: parseConfig,
retriever: retriever,
}
notaryCmd.AddCommand(cmdKeyGenerator.GetCommand())
notaryCmd.AddCommand(cmdDelegationGenerator.GetCommand())
notaryCmd.AddCommand(cmdCert)
notaryCmd.AddCommand(cmdTufInit)
notaryCmd.AddCommand(cmdTufList)

View File

@@ -8,6 +8,7 @@ import (
"math"
"path/filepath"
"sort"
"strings"
"time"
"github.com/docker/notary/client"
@@ -142,8 +143,17 @@ func (t targetsSorter) Less(i, j int) bool {
return t[i].Name < t[j].Name
}
// Given a list of KeyStores in order of listing preference, pretty-prints the
// root keys and then the signing keys.
// --- pretty printing roles ---
type roleSorter []*data.Role
func (r roleSorter) Len() int { return len(r) }
func (r roleSorter) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
func (r roleSorter) Less(i, j int) bool {
return r[i].Name < r[j].Name
}
// Pretty-prints the sorted list of TargetWithRoles.
func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) {
if len(ts) == 0 {
writer.Write([]byte("\nNo targets present in this repository.\n\n"))
@@ -165,6 +175,29 @@ func prettyPrintTargets(ts []*client.TargetWithRole, writer io.Writer) {
table.Render()
}
// Pretty-prints the list of provided Roles
func prettyPrintRoles(rs []*data.Role, writer io.Writer) {
if len(rs) == 0 {
writer.Write([]byte("\nNo such roles published in this repository.\n\n"))
return
}
// this sorter works for Role types
sort.Stable(roleSorter(rs))
table := getTable([]string{"Role", "Paths", "Key IDs", "Threshold"}, writer)
for _, r := range rs {
table.Append([]string{
r.Name,
strings.Join(r.Paths, ","),
strings.Join(r.KeyIDs, ","),
fmt.Sprintf("%v", r.Threshold),
})
}
table.Render()
}
// --- pretty printing certs ---
// cert by repo name then expiry time. Don't bother sorting by fingerprint.

View File

@@ -214,6 +214,58 @@ func generateCertificate(t *testing.T, gun string, expireInHours int64) *x509.Ce
return cert
}
// --- tests for pretty printing roles ---
// If there are no roles, no table is printed, only a line saying that there
// are no roles.
func TestPrettyPrintZeroRoles(t *testing.T) {
var b bytes.Buffer
prettyPrintRoles([]*data.Role{}, &b)
text, err := ioutil.ReadAll(&b)
assert.NoError(t, err)
lines := strings.Split(strings.TrimSpace(string(text)), "\n")
assert.Len(t, lines, 1)
assert.Equal(t, "No such roles published in this repository.", lines[0])
}
// Roles are sorted by name, and the name, paths, and KeyIDs are printed.
func TestPrettyPrintSortedRoles(t *testing.T) {
var err error
unsorted := []*data.Role{
{Name: "targets/zebra", Paths: []string{"stripes", "black", "white"}, RootRole: data.RootRole{KeyIDs: []string{"101"}, Threshold: 1}},
{Name: "targets/aardvark/unicorn/pony", Paths: []string{"rainbows"}, RootRole: data.RootRole{KeyIDs: []string{"135"}, Threshold: 1}},
{Name: "targets/bee", Paths: []string{"honey"}, RootRole: data.RootRole{KeyIDs: []string{"246"}, Threshold: 1}},
{Name: "targets/bee/wasp", Paths: []string{"honey/sting"}, RootRole: data.RootRole{KeyIDs: []string{"246", "468"}, Threshold: 1}},
}
var b bytes.Buffer
prettyPrintRoles(unsorted, &b)
text, err := ioutil.ReadAll(&b)
assert.NoError(t, err)
expected := [][]string{
{"targets/aardvark/unicorn/pony", "rainbows", "135", "1"},
{"targets/bee", "honey", "246", "1"},
{"targets/bee/wasp", "honey/sting", "246,468", "1"},
{"targets/zebra", "stripes,black,white", "101", "1"},
}
lines := strings.Split(strings.TrimSpace(string(text)), "\n")
assert.Len(t, lines, len(expected)+2)
// starts with headers
assert.True(t, reflect.DeepEqual(strings.Fields(lines[0]), strings.Fields(
"ROLE PATHS KEY IDS THRESHOLD")))
assert.Equal(t, "----", lines[1][:4])
for i, line := range lines[2:] {
splitted := strings.Fields(line)
assert.Equal(t, expected[i], splitted)
}
}
// If there are no certs in the cert store store, a message that there are no
// certs should be displayed.
func TestPrettyPrintZeroCerts(t *testing.T) {