mirror of https://github.com/docker/docs.git
Merge pull request #308 from docker/pretty-print-certs
Pretty-print certificates from the notary CLI command `notary cert list`
This commit is contained in:
commit
909260ff03
|
@ -2,9 +2,7 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"math"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/notary/certs"
|
"github.com/docker/notary/certs"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
|
@ -124,20 +122,9 @@ func certList(cmd *cobra.Command, args []string) {
|
||||||
fatalf("Failed to create a new truststore manager with directory: %s", trustDir)
|
fatalf("Failed to create a new truststore manager with directory: %s", trustDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Println("")
|
|
||||||
cmd.Println("# Trusted Certificates:")
|
|
||||||
trustedCerts := certManager.TrustedCertificateStore().GetCertificates()
|
trustedCerts := certManager.TrustedCertificateStore().GetCertificates()
|
||||||
for _, c := range trustedCerts {
|
|
||||||
printCert(cmd, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func printCert(cmd *cobra.Command, cert *x509.Certificate) {
|
cmd.Println("")
|
||||||
timeDifference := cert.NotAfter.Sub(time.Now())
|
prettyPrintCerts(trustedCerts, cmd.Out())
|
||||||
certID, err := trustmanager.FingerprintCert(cert)
|
cmd.Println("")
|
||||||
if err != nil {
|
|
||||||
fatalf("Could not fingerprint certificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Printf("%s %s (expires in: %v days)\n", cert.Subject.CommonName, certID, math.Floor(timeDifference.Hours()/24))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -498,11 +498,16 @@ func TestClientKeyImportExportRootOnly(t *testing.T) {
|
||||||
func assertNumCerts(t *testing.T, tempDir string, expectedNum int) []string {
|
func assertNumCerts(t *testing.T, tempDir string, expectedNum int) []string {
|
||||||
output, err := runCommand(t, tempDir, "cert", "list")
|
output, err := runCommand(t, tempDir, "cert", "list")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
certs := splitLines(
|
lines := splitLines(strings.TrimSpace(output))
|
||||||
strings.TrimPrefix(strings.TrimSpace(output), "# Trusted Certificates:"))
|
|
||||||
|
|
||||||
assert.Len(t, certs, expectedNum)
|
if expectedNum == 0 {
|
||||||
return certs
|
assert.Len(t, lines, 1)
|
||||||
|
assert.Equal(t, "No trusted root certificates present.", lines[0])
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Len(t, lines, expectedNum+2)
|
||||||
|
return lines[2:]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestClientCertInteraction
|
// TestClientCertInteraction
|
||||||
|
@ -530,7 +535,7 @@ func TestClientCertInteraction(t *testing.T) {
|
||||||
certs = assertNumCerts(t, tempDir, 1)
|
certs = assertNumCerts(t, tempDir, 1)
|
||||||
|
|
||||||
// remove a single cert
|
// remove a single cert
|
||||||
certID := strings.TrimSpace(strings.Split(certs[0], " ")[1])
|
certID := strings.Fields(certs[0])[1]
|
||||||
// passing an empty gun here because the string for the previous gun has
|
// passing an empty gun here because the string for the previous gun has
|
||||||
// has already been stored (a drawback of running these commands without)
|
// has already been stored (a drawback of running these commands without)
|
||||||
// shelling out
|
// shelling out
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -17,7 +16,6 @@ import (
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
|
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -101,110 +99,6 @@ var cmdKeyRemove = &cobra.Command{
|
||||||
Run: keyRemove,
|
Run: keyRemove,
|
||||||
}
|
}
|
||||||
|
|
||||||
func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string {
|
|
||||||
if len(str) <= maxWidth {
|
|
||||||
return str
|
|
||||||
}
|
|
||||||
if leftTruncate {
|
|
||||||
return fmt.Sprintf("...%s", str[len(str)-(maxWidth-3):])
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s...", str[:maxWidth-3])
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
maxGUNWidth = 25
|
|
||||||
maxLocWidth = 40
|
|
||||||
)
|
|
||||||
|
|
||||||
type keyInfo struct {
|
|
||||||
gun string // assumption that this is "" if role is root
|
|
||||||
role string
|
|
||||||
keyID string
|
|
||||||
location string
|
|
||||||
}
|
|
||||||
|
|
||||||
// We want to sort by gun, then by role, then by keyID, then by location
|
|
||||||
// In the case of a root role, then there is no GUN, and a root role comes
|
|
||||||
// first.
|
|
||||||
type keyInfoSorter []keyInfo
|
|
||||||
|
|
||||||
func (k keyInfoSorter) Len() int { return len(k) }
|
|
||||||
func (k keyInfoSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
|
|
||||||
func (k keyInfoSorter) Less(i, j int) bool {
|
|
||||||
// special-case role
|
|
||||||
if k[i].role != k[j].role {
|
|
||||||
if k[i].role == data.CanonicalRootRole {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if k[j].role == data.CanonicalRootRole {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// otherwise, neither of them are root, they're just different, so
|
|
||||||
// go with the traditional sort order.
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort order is GUN, role, keyID, location.
|
|
||||||
orderedI := []string{k[i].gun, k[i].role, k[i].keyID, k[i].location}
|
|
||||||
orderedJ := []string{k[j].gun, k[j].role, k[j].keyID, k[j].location}
|
|
||||||
|
|
||||||
for x := 0; x < 4; x++ {
|
|
||||||
switch {
|
|
||||||
case orderedI[x] < orderedJ[x]:
|
|
||||||
return true
|
|
||||||
case orderedI[x] > orderedJ[x]:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// continue on and evalulate the next item
|
|
||||||
}
|
|
||||||
// this shouldn't happen - that means two values are exactly equal
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a list of KeyStores in order of listing preference, pretty-prints the
|
|
||||||
// root keys and then the signing keys.
|
|
||||||
func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) {
|
|
||||||
var info []keyInfo
|
|
||||||
|
|
||||||
for _, store := range keyStores {
|
|
||||||
for keyPath, role := range store.ListKeys() {
|
|
||||||
gun := ""
|
|
||||||
if role != data.CanonicalRootRole {
|
|
||||||
gun = filepath.Dir(keyPath)
|
|
||||||
}
|
|
||||||
info = append(info, keyInfo{
|
|
||||||
role: role,
|
|
||||||
location: store.Name(),
|
|
||||||
gun: gun,
|
|
||||||
keyID: filepath.Base(keyPath),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(info) == 0 {
|
|
||||||
writer.Write([]byte("No signing keys found.\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Stable(keyInfoSorter(info))
|
|
||||||
|
|
||||||
table := tablewriter.NewWriter(writer)
|
|
||||||
table.SetHeader([]string{"ROLE", "GUN", "KEY ID", "LOCATION"})
|
|
||||||
table.SetBorder(false)
|
|
||||||
table.SetColumnSeparator(" ")
|
|
||||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
||||||
table.SetCenterSeparator("-")
|
|
||||||
table.SetAutoWrapText(false)
|
|
||||||
|
|
||||||
for _, oneKeyInfo := range info {
|
|
||||||
table.Append([]string{
|
|
||||||
oneKeyInfo.role,
|
|
||||||
truncateWithEllipsis(oneKeyInfo.gun, maxGUNWidth, true),
|
|
||||||
oneKeyInfo.keyID,
|
|
||||||
truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
table.Render()
|
|
||||||
}
|
|
||||||
|
|
||||||
func keysList(cmd *cobra.Command, args []string) {
|
func keysList(cmd *cobra.Command, args []string) {
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
cmd.Usage()
|
cmd.Usage()
|
||||||
|
@ -398,12 +292,6 @@ func keysImportRoot(cmd *cobra.Command, args []string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printKey(cmd *cobra.Command, keyPath, alias, loc string) {
|
|
||||||
keyID := filepath.Base(keyPath)
|
|
||||||
gun := filepath.Dir(keyPath)
|
|
||||||
cmd.Printf("%s - %s - %s - %s\n", gun, alias, keyID, loc)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keysRotate(cmd *cobra.Command, args []string) {
|
func keysRotate(cmd *cobra.Command, args []string) {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
cmd.Usage()
|
cmd.Usage()
|
||||||
|
|
|
@ -3,139 +3,17 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"reflect"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/notary/passphrase"
|
"github.com/docker/notary/passphrase"
|
||||||
"github.com/docker/notary/trustmanager"
|
"github.com/docker/notary/trustmanager"
|
||||||
"github.com/docker/notary/tuf/data"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ret = passphrase.ConstantRetriever("pass")
|
var ret = passphrase.ConstantRetriever("pass")
|
||||||
|
|
||||||
func TestTruncateWithEllipsis(t *testing.T) {
|
|
||||||
digits := "1234567890"
|
|
||||||
// do not truncate
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 10, true), digits)
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 10, false), digits)
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 11, true), digits)
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 11, false), digits)
|
|
||||||
|
|
||||||
// left and right truncate
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 8, true), "...67890")
|
|
||||||
assert.Equal(t, truncateWithEllipsis(digits, 8, false), "12345...")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeyInfoSorter(t *testing.T) {
|
|
||||||
expected := []keyInfo{
|
|
||||||
{role: data.CanonicalRootRole, gun: "", keyID: "a", location: "i"},
|
|
||||||
{role: data.CanonicalRootRole, gun: "", keyID: "a", location: "j"},
|
|
||||||
{role: data.CanonicalRootRole, gun: "", keyID: "z", location: "z"},
|
|
||||||
{role: "a", gun: "a", keyID: "a", location: "y"},
|
|
||||||
{role: "b", gun: "a", keyID: "a", location: "y"},
|
|
||||||
{role: "b", gun: "a", keyID: "b", location: "y"},
|
|
||||||
{role: "b", gun: "a", keyID: "b", location: "z"},
|
|
||||||
{role: "a", gun: "b", keyID: "a", location: "z"},
|
|
||||||
}
|
|
||||||
jumbled := make([]keyInfo, len(expected))
|
|
||||||
// randomish indices
|
|
||||||
for j, e := range []int{3, 6, 1, 4, 0, 7, 5, 2} {
|
|
||||||
jumbled[j] = expected[e]
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Sort(keyInfoSorter(jumbled))
|
|
||||||
assert.True(t, reflect.DeepEqual(expected, jumbled),
|
|
||||||
fmt.Sprintf("Expected %v, Got %v", expected, jumbled))
|
|
||||||
}
|
|
||||||
|
|
||||||
type otherMemoryStore struct {
|
|
||||||
trustmanager.KeyMemoryStore
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *otherMemoryStore) Name() string {
|
|
||||||
return strings.Repeat("z", 70)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a list of key stores, the keys should be pretty-printed with their
|
|
||||||
// roles, locations, IDs, and guns first in sorted order in the key store
|
|
||||||
func TestPrettyPrintRootAndSigningKeys(t *testing.T) {
|
|
||||||
ret := passphrase.ConstantRetriever("pass")
|
|
||||||
keyStores := []trustmanager.KeyStore{
|
|
||||||
trustmanager.NewKeyMemoryStore(ret),
|
|
||||||
&otherMemoryStore{KeyMemoryStore: *trustmanager.NewKeyMemoryStore(ret)},
|
|
||||||
}
|
|
||||||
|
|
||||||
longNameShortened := "..." + strings.Repeat("z", 37)
|
|
||||||
|
|
||||||
// just use the same key for testing
|
|
||||||
key, err := trustmanager.GenerateED25519Key(rand.Reader)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
root := data.CanonicalRootRole
|
|
||||||
|
|
||||||
// add keys to the key stores
|
|
||||||
err = keyStores[0].AddKey(key.ID(), root, key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = keyStores[1].AddKey(key.ID(), root, key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = keyStores[0].AddKey(strings.Repeat("a/", 30)+key.ID(), "targets", key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = keyStores[1].AddKey("short/gun/"+key.ID(), "snapshot", key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
expected := [][]string{
|
|
||||||
{root, key.ID(), keyStores[0].Name()},
|
|
||||||
{root, key.ID(), longNameShortened},
|
|
||||||
{"targets", "..." + strings.Repeat("/a", 11), key.ID(), keyStores[0].Name()},
|
|
||||||
{"snapshot", "short/gun", key.ID(), longNameShortened},
|
|
||||||
}
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
prettyPrintKeys(keyStores, &b)
|
|
||||||
text, err := ioutil.ReadAll(&b)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
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]),
|
|
||||||
[]string{"ROLE", "GUN", "KEY", "ID", "LOCATION"}))
|
|
||||||
assert.Equal(t, "----", lines[1][:4])
|
|
||||||
|
|
||||||
for i, line := range lines[2:] {
|
|
||||||
// we are purposely not putting spaces in test data so easier to split
|
|
||||||
splitted := strings.Fields(line)
|
|
||||||
for j, v := range splitted {
|
|
||||||
assert.Equal(t, expected[i][j], strings.TrimSpace(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no keys in any of the key stores, a message that there are no
|
|
||||||
// signing keys should be displayed.
|
|
||||||
func TestPrettyPrintZeroKeys(t *testing.T) {
|
|
||||||
ret := passphrase.ConstantRetriever("pass")
|
|
||||||
emptyKeyStore := trustmanager.NewKeyMemoryStore(ret)
|
|
||||||
|
|
||||||
var b bytes.Buffer
|
|
||||||
prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &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 signing keys found.", lines[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there are no keys, removeKeyInteractively will just return an error about
|
// If there are no keys, removeKeyInteractively will just return an error about
|
||||||
// there not being any key
|
// there not being any key
|
||||||
func TestRemoveIfNoKey(t *testing.T) {
|
func TestRemoveIfNoKey(t *testing.T) {
|
||||||
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/notary/client"
|
||||||
|
"github.com/docker/notary/trustmanager"
|
||||||
|
"github.com/docker/notary/tuf/data"
|
||||||
|
"github.com/olekukonko/tablewriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
// returns a tablewriter
|
||||||
|
func getTable(headers []string, writer io.Writer) *tablewriter.Table {
|
||||||
|
table := tablewriter.NewWriter(writer)
|
||||||
|
table.SetBorder(false)
|
||||||
|
table.SetColumnSeparator(" ")
|
||||||
|
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
||||||
|
table.SetCenterSeparator("-")
|
||||||
|
table.SetAutoWrapText(false)
|
||||||
|
table.SetHeader(headers)
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- pretty printing certs ---
|
||||||
|
|
||||||
|
func truncateWithEllipsis(str string, maxWidth int, leftTruncate bool) string {
|
||||||
|
if len(str) <= maxWidth {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
if leftTruncate {
|
||||||
|
return fmt.Sprintf("...%s", str[len(str)-(maxWidth-3):])
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s...", str[:maxWidth-3])
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxGUNWidth = 25
|
||||||
|
maxLocWidth = 40
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyInfo struct {
|
||||||
|
gun string // assumption that this is "" if role is root
|
||||||
|
role string
|
||||||
|
keyID string
|
||||||
|
location string
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to sort by gun, then by role, then by keyID, then by location
|
||||||
|
// In the case of a root role, then there is no GUN, and a root role comes
|
||||||
|
// first.
|
||||||
|
type keyInfoSorter []keyInfo
|
||||||
|
|
||||||
|
func (k keyInfoSorter) Len() int { return len(k) }
|
||||||
|
func (k keyInfoSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
|
||||||
|
func (k keyInfoSorter) Less(i, j int) bool {
|
||||||
|
// special-case role
|
||||||
|
if k[i].role != k[j].role {
|
||||||
|
if k[i].role == data.CanonicalRootRole {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if k[j].role == data.CanonicalRootRole {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// otherwise, neither of them are root, they're just different, so
|
||||||
|
// go with the traditional sort order.
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort order is GUN, role, keyID, location.
|
||||||
|
orderedI := []string{k[i].gun, k[i].role, k[i].keyID, k[i].location}
|
||||||
|
orderedJ := []string{k[j].gun, k[j].role, k[j].keyID, k[j].location}
|
||||||
|
|
||||||
|
for x := 0; x < 4; x++ {
|
||||||
|
switch {
|
||||||
|
case orderedI[x] < orderedJ[x]:
|
||||||
|
return true
|
||||||
|
case orderedI[x] > orderedJ[x]:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// continue on and evalulate the next item
|
||||||
|
}
|
||||||
|
// this shouldn't happen - that means two values are exactly equal
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a list of KeyStores in order of listing preference, pretty-prints the
|
||||||
|
// root keys and then the signing keys.
|
||||||
|
func prettyPrintKeys(keyStores []trustmanager.KeyStore, writer io.Writer) {
|
||||||
|
var info []keyInfo
|
||||||
|
|
||||||
|
for _, store := range keyStores {
|
||||||
|
for keyPath, role := range store.ListKeys() {
|
||||||
|
gun := ""
|
||||||
|
if role != data.CanonicalRootRole {
|
||||||
|
gun = filepath.Dir(keyPath)
|
||||||
|
}
|
||||||
|
info = append(info, keyInfo{
|
||||||
|
role: role,
|
||||||
|
location: store.Name(),
|
||||||
|
gun: gun,
|
||||||
|
keyID: filepath.Base(keyPath),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(info) == 0 {
|
||||||
|
writer.Write([]byte("No signing keys found.\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Stable(keyInfoSorter(info))
|
||||||
|
|
||||||
|
table := getTable([]string{"ROLE", "GUN", "KEY ID", "LOCATION"}, writer)
|
||||||
|
|
||||||
|
for _, oneKeyInfo := range info {
|
||||||
|
table.Append([]string{
|
||||||
|
oneKeyInfo.role,
|
||||||
|
truncateWithEllipsis(oneKeyInfo.gun, maxGUNWidth, true),
|
||||||
|
oneKeyInfo.keyID,
|
||||||
|
truncateWithEllipsis(oneKeyInfo.location, maxLocWidth, true),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- pretty printing targets ---
|
||||||
|
|
||||||
|
type targetsSorter []*client.Target
|
||||||
|
|
||||||
|
func (t targetsSorter) Len() int { return len(t) }
|
||||||
|
func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||||
|
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.
|
||||||
|
func prettyPrintTargets(ts []*client.Target, writer io.Writer) {
|
||||||
|
if len(ts) == 0 {
|
||||||
|
writer.Write([]byte("\nNo targets present in this repository.\n\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Stable(targetsSorter(ts))
|
||||||
|
|
||||||
|
table := getTable([]string{"Name", "Digest", "Size (bytes)"}, writer)
|
||||||
|
|
||||||
|
for _, t := range ts {
|
||||||
|
table.Append([]string{
|
||||||
|
t.Name,
|
||||||
|
hex.EncodeToString(t.Hashes["sha256"]),
|
||||||
|
fmt.Sprintf("%d", t.Length),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- pretty printing certs ---
|
||||||
|
|
||||||
|
// cert by repo name then expiry time. Don't bother sorting by fingerprint.
|
||||||
|
type certSorter []*x509.Certificate
|
||||||
|
|
||||||
|
func (t certSorter) Len() int { return len(t) }
|
||||||
|
func (t certSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||||
|
func (t certSorter) Less(i, j int) bool {
|
||||||
|
if t[i].Subject.CommonName < t[j].Subject.CommonName {
|
||||||
|
return true
|
||||||
|
} else if t[i].Subject.CommonName > t[j].Subject.CommonName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t[i].NotAfter.Before(t[j].NotAfter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a list of Ceritifcates in order of listing preference, pretty-prints
|
||||||
|
// the cert common name, fingerprint, and expiry
|
||||||
|
func prettyPrintCerts(certs []*x509.Certificate, writer io.Writer) {
|
||||||
|
if len(certs) == 0 {
|
||||||
|
writer.Write([]byte("\nNo trusted root certificates present.\n\n"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Stable(certSorter(certs))
|
||||||
|
|
||||||
|
table := getTable([]string{
|
||||||
|
"GUN", "Fingerprint of Trusted Root Certificate", "Expires In"}, writer)
|
||||||
|
|
||||||
|
for _, c := range certs {
|
||||||
|
days := math.Floor(c.NotAfter.Sub(time.Now()).Hours() / 24)
|
||||||
|
expiryString := "< 1 day"
|
||||||
|
if days == 1 {
|
||||||
|
expiryString = "1 day"
|
||||||
|
} else if days > 1 {
|
||||||
|
expiryString = fmt.Sprintf("%d days", int(days))
|
||||||
|
}
|
||||||
|
|
||||||
|
certID, err := trustmanager.FingerprintCert(c)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Could not fingerprint certificate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Append([]string{c.Subject.CommonName, certID, expiryString})
|
||||||
|
}
|
||||||
|
table.Render()
|
||||||
|
}
|
|
@ -0,0 +1,269 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/notary/client"
|
||||||
|
"github.com/docker/notary/passphrase"
|
||||||
|
"github.com/docker/notary/trustmanager"
|
||||||
|
"github.com/docker/notary/tuf/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- tests for pretty printing keys ---
|
||||||
|
|
||||||
|
func TestTruncateWithEllipsis(t *testing.T) {
|
||||||
|
digits := "1234567890"
|
||||||
|
// do not truncate
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 10, true), digits)
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 10, false), digits)
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 11, true), digits)
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 11, false), digits)
|
||||||
|
|
||||||
|
// left and right truncate
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 8, true), "...67890")
|
||||||
|
assert.Equal(t, truncateWithEllipsis(digits, 8, false), "12345...")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKeyInfoSorter(t *testing.T) {
|
||||||
|
expected := []keyInfo{
|
||||||
|
{role: data.CanonicalRootRole, gun: "", keyID: "a", location: "i"},
|
||||||
|
{role: data.CanonicalRootRole, gun: "", keyID: "a", location: "j"},
|
||||||
|
{role: data.CanonicalRootRole, gun: "", keyID: "z", location: "z"},
|
||||||
|
{role: "a", gun: "a", keyID: "a", location: "y"},
|
||||||
|
{role: "b", gun: "a", keyID: "a", location: "y"},
|
||||||
|
{role: "b", gun: "a", keyID: "b", location: "y"},
|
||||||
|
{role: "b", gun: "a", keyID: "b", location: "z"},
|
||||||
|
{role: "a", gun: "b", keyID: "a", location: "z"},
|
||||||
|
}
|
||||||
|
jumbled := make([]keyInfo, len(expected))
|
||||||
|
// randomish indices
|
||||||
|
for j, e := range []int{3, 6, 1, 4, 0, 7, 5, 2} {
|
||||||
|
jumbled[j] = expected[e]
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Sort(keyInfoSorter(jumbled))
|
||||||
|
assert.True(t, reflect.DeepEqual(expected, jumbled),
|
||||||
|
fmt.Sprintf("Expected %v, Got %v", expected, jumbled))
|
||||||
|
}
|
||||||
|
|
||||||
|
type otherMemoryStore struct {
|
||||||
|
trustmanager.KeyMemoryStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *otherMemoryStore) Name() string {
|
||||||
|
return strings.Repeat("z", 70)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no keys in any of the key stores, a message that there are no
|
||||||
|
// signing keys should be displayed.
|
||||||
|
func TestPrettyPrintZeroKeys(t *testing.T) {
|
||||||
|
ret := passphrase.ConstantRetriever("pass")
|
||||||
|
emptyKeyStore := trustmanager.NewKeyMemoryStore(ret)
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintKeys([]trustmanager.KeyStore{emptyKeyStore}, &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 signing keys found.", lines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a list of key stores, the keys should be pretty-printed with their
|
||||||
|
// roles, locations, IDs, and guns first in sorted order in the key store
|
||||||
|
func TestPrettyPrintRootAndSigningKeys(t *testing.T) {
|
||||||
|
ret := passphrase.ConstantRetriever("pass")
|
||||||
|
keyStores := []trustmanager.KeyStore{
|
||||||
|
trustmanager.NewKeyMemoryStore(ret),
|
||||||
|
&otherMemoryStore{KeyMemoryStore: *trustmanager.NewKeyMemoryStore(ret)},
|
||||||
|
}
|
||||||
|
|
||||||
|
longNameShortened := "..." + strings.Repeat("z", 37)
|
||||||
|
|
||||||
|
// just use the same key for testing
|
||||||
|
key, err := trustmanager.GenerateED25519Key(rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
root := data.CanonicalRootRole
|
||||||
|
|
||||||
|
// add keys to the key stores
|
||||||
|
err = keyStores[0].AddKey(key.ID(), root, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = keyStores[1].AddKey(key.ID(), root, key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = keyStores[0].AddKey(strings.Repeat("a/", 30)+key.ID(), "targets", key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = keyStores[1].AddKey("short/gun/"+key.ID(), "snapshot", key)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected := [][]string{
|
||||||
|
{root, key.ID(), keyStores[0].Name()},
|
||||||
|
{root, key.ID(), longNameShortened},
|
||||||
|
{"targets", "..." + strings.Repeat("/a", 11), key.ID(), keyStores[0].Name()},
|
||||||
|
{"snapshot", "short/gun", key.ID(), longNameShortened},
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintKeys(keyStores, &b)
|
||||||
|
text, err := ioutil.ReadAll(&b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
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]),
|
||||||
|
[]string{"ROLE", "GUN", "KEY", "ID", "LOCATION"}))
|
||||||
|
assert.Equal(t, "----", lines[1][:4])
|
||||||
|
|
||||||
|
for i, line := range lines[2:] {
|
||||||
|
// we are purposely not putting spaces in test data so easier to split
|
||||||
|
splitted := strings.Fields(line)
|
||||||
|
for j, v := range splitted {
|
||||||
|
assert.Equal(t, expected[i][j], strings.TrimSpace(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests for pretty printing targets ---
|
||||||
|
|
||||||
|
// If there are no targets, no table is printed, only a line saying that there
|
||||||
|
// are no targets.
|
||||||
|
func TestPrettyPrintZeroTargets(t *testing.T) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintTargets([]*client.Target{}, &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 targets present in this repository.", lines[0])
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Targets are sorted by name, and the name, SHA256 digest, and size are
|
||||||
|
// printed.
|
||||||
|
func TestPrettyPrintSortedTargets(t *testing.T) {
|
||||||
|
hashes := make([][]byte, 3)
|
||||||
|
var err error
|
||||||
|
for i, letter := range []string{"a012", "b012", "c012"} {
|
||||||
|
hashes[i], err = hex.DecodeString(letter)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
unsorted := []*client.Target{
|
||||||
|
{Name: "zebra", Hashes: data.Hashes{"sha256": hashes[0]}, Length: 8},
|
||||||
|
{Name: "abracadabra", Hashes: data.Hashes{"sha256": hashes[1]}, Length: 1},
|
||||||
|
{Name: "bee", Hashes: data.Hashes{"sha256": hashes[2]}, Length: 5},
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintTargets(unsorted, &b)
|
||||||
|
text, err := ioutil.ReadAll(&b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected := [][]string{
|
||||||
|
{"abracadabra", "b012", "1"},
|
||||||
|
{"bee", "c012", "5"},
|
||||||
|
{"zebra", "a012", "8"},
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"NAME DIGEST SIZE (BYTES)")))
|
||||||
|
assert.Equal(t, "----", lines[1][:4])
|
||||||
|
|
||||||
|
for i, line := range lines[2:] {
|
||||||
|
splitted := strings.Fields(line)
|
||||||
|
assert.Equal(t, expected[i], splitted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tests for pretty printing certs ---
|
||||||
|
|
||||||
|
func generateCertificate(t *testing.T, gun string, expireInHours int64) *x509.Certificate {
|
||||||
|
template, err := trustmanager.NewCertificate(gun)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
template.NotAfter = template.NotBefore.Add(
|
||||||
|
time.Hour * time.Duration(expireInHours))
|
||||||
|
|
||||||
|
ecdsaPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
certBytes, err := x509.CreateCertificate(rand.Reader, template, template,
|
||||||
|
ecdsaPrivKey.Public(), ecdsaPrivKey)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(certBytes)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintCerts([]*x509.Certificate{}, &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 trusted root certificates present.", lines[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificates are pretty-printed in table form sorted by gun and then expiry
|
||||||
|
func TestPrettyPrintSortedCerts(t *testing.T) {
|
||||||
|
unsorted := []*x509.Certificate{
|
||||||
|
generateCertificate(t, "xylitol", 77), // 3 days 5 hours
|
||||||
|
generateCertificate(t, "xylitol", 12), // less than 1 day
|
||||||
|
generateCertificate(t, "cheesecake", 25), // a little more than 1 day
|
||||||
|
generateCertificate(t, "baklava", 239), // almost 10 days
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
prettyPrintCerts(unsorted, &b)
|
||||||
|
text, err := ioutil.ReadAll(&b)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected := [][]string{
|
||||||
|
{"baklava", "9 days"},
|
||||||
|
{"cheesecake", "1 day"},
|
||||||
|
{"xylitol", "< 1 day"},
|
||||||
|
{"xylitol", "3 days"},
|
||||||
|
}
|
||||||
|
|
||||||
|
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(
|
||||||
|
"GUN FINGERPRINT OF TRUSTED ROOT CERTIFICATE EXPIRES IN")))
|
||||||
|
assert.Equal(t, "----", lines[1][:4])
|
||||||
|
|
||||||
|
for i, line := range lines[2:] {
|
||||||
|
splitted := strings.Fields(line)
|
||||||
|
assert.True(t, len(splitted) >= 3)
|
||||||
|
assert.Equal(t, expected[i][0], splitted[0])
|
||||||
|
assert.Equal(t, expected[i][1], strings.Join(splitted[2:], " "))
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,16 +3,13 @@ package main
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -25,7 +22,6 @@ import (
|
||||||
notaryclient "github.com/docker/notary/client"
|
notaryclient "github.com/docker/notary/client"
|
||||||
"github.com/docker/notary/tuf/data"
|
"github.com/docker/notary/tuf/data"
|
||||||
"github.com/docker/notary/utils"
|
"github.com/docker/notary/utils"
|
||||||
"github.com/olekukonko/tablewriter"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -448,39 +444,3 @@ func getRemoteTrustServer() string {
|
||||||
}
|
}
|
||||||
return remoteTrustServer
|
return remoteTrustServer
|
||||||
}
|
}
|
||||||
|
|
||||||
type targetsSorter []*notaryclient.Target
|
|
||||||
|
|
||||||
func (t targetsSorter) Len() int { return len(t) }
|
|
||||||
func (t targetsSorter) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
|
||||||
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.
|
|
||||||
func prettyPrintTargets(ts []*notaryclient.Target, writer io.Writer) {
|
|
||||||
if len(ts) == 0 {
|
|
||||||
writer.Write([]byte("\nNo targets present in this repository.\n\n"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Stable(targetsSorter(ts))
|
|
||||||
|
|
||||||
table := tablewriter.NewWriter(writer)
|
|
||||||
table.SetHeader([]string{"Name", "Digest", "Size (bytes)"})
|
|
||||||
table.SetBorder(false)
|
|
||||||
table.SetColumnSeparator(" ")
|
|
||||||
table.SetAlignment(tablewriter.ALIGN_LEFT)
|
|
||||||
table.SetCenterSeparator("-")
|
|
||||||
table.SetAutoWrapText(false)
|
|
||||||
|
|
||||||
for _, t := range ts {
|
|
||||||
table.Append([]string{
|
|
||||||
t.Name,
|
|
||||||
hex.EncodeToString(t.Hashes["sha256"]),
|
|
||||||
fmt.Sprintf("%d", t.Length),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
table.Render()
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue