mirror of https://github.com/docker/docs.git
Merge pull request #12 from docker/filestore-refactor
Major refactor the the FileStore
This commit is contained in:
commit
5efc8c0549
|
@ -9,7 +9,6 @@ import (
|
|||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/docker/notary/trustmanager"
|
||||
|
@ -113,28 +112,8 @@ func generateKeyAndCert(gun string) (crypto.PrivateKey, *x509.Certificate, error
|
|||
|
||||
kID := trustmanager.FingerprintCert(cert)
|
||||
// The key is going to be stored in the private directory, using the GUN and
|
||||
// the filename will be the TUF-compliant ID
|
||||
privKeyFilename := filepath.Join(viper.GetString("privDir"), gun, string(kID)+".key")
|
||||
|
||||
// If GUN is in the form of 'foo/bar' ensures that private key is stored in the
|
||||
// adequate sub-directory
|
||||
err = trustmanager.CreateDirectory(privKeyFilename)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not create directory for private key: %v", err)
|
||||
}
|
||||
|
||||
// Opens a FD to the file with the correct permissions
|
||||
keyOut, err := os.OpenFile(privKeyFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("could not write privatekey: %v", err)
|
||||
}
|
||||
defer keyOut.Close()
|
||||
|
||||
// Encodes the private key as PEM and writes it
|
||||
err = pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encode key: %v", err)
|
||||
}
|
||||
|
||||
// the filename will be the TUF-compliant ID. The Store takes care of extensions.
|
||||
privKeyFilename := filepath.Join(gun, string(kID))
|
||||
privKeyStore.Add(privKeyFilename, trustmanager.KeyToPEM(keyBytes))
|
||||
return key, cert, nil
|
||||
}
|
||||
|
|
|
@ -55,29 +55,55 @@ var cmdKeysGenerate = &cobra.Command{
|
|||
Run: keysGenerate,
|
||||
}
|
||||
|
||||
// keysRemove deletes Certificates based on hash and Private Keys
|
||||
// based on GUNs.
|
||||
func keysRemove(cmd *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
cmd.Usage()
|
||||
fatalf("must specify a SHA256 SubjectKeyID of the certificate")
|
||||
}
|
||||
|
||||
failed := true
|
||||
cert, err := caStore.GetCertificateBykID(args[0])
|
||||
//TODO (diogo): Validate Global Unique Name. We probably want to reject 1 char GUNs.
|
||||
gunOrID := args[0]
|
||||
|
||||
// Try to retreive the ID from the CA store.
|
||||
cert, err := caStore.GetCertificateBykID(gunOrID)
|
||||
if err == nil {
|
||||
fmt.Printf("Removing: ")
|
||||
printCert(cert)
|
||||
|
||||
// If the ID is found, remove it.
|
||||
err = caStore.RemoveCert(cert)
|
||||
if err != nil {
|
||||
fatalf("failed to remove certificate from KeyStore")
|
||||
}
|
||||
failed = false
|
||||
return
|
||||
}
|
||||
|
||||
//TODO (diogo): We might want to delete private keys from the CLI
|
||||
if failed {
|
||||
fatalf("certificate not found in any store")
|
||||
// We didn't find a certificate with this ID, let's try to see if we can find keys.
|
||||
keyList := privKeyStore.ListGUN(gunOrID)
|
||||
if len(keyList) < 1 {
|
||||
fatalf("no Private Keys found under Global Unique Name: %s", gunOrID)
|
||||
}
|
||||
|
||||
// List all the keys about to be removed
|
||||
fmt.Println("Are you sure you want to remove the following keys? (yes/no)", gunOrID)
|
||||
for _, k := range keyList {
|
||||
printKey(k)
|
||||
}
|
||||
|
||||
// Ask for confirmation before removing keys
|
||||
confirmed := askConfirm()
|
||||
if !confirmed {
|
||||
fatalf("aborting action.")
|
||||
}
|
||||
|
||||
// Remove all the keys under the Global Unique Name
|
||||
err = privKeyStore.RemoveGUN(gunOrID)
|
||||
if err != nil {
|
||||
fatalf("failed to remove all Private keys under Global Unique Name: %s", gunOrID)
|
||||
}
|
||||
fmt.Printf("Removing all Private keys from: %s \n", gunOrID)
|
||||
}
|
||||
|
||||
//TODO (diogo): Ask the use if she wants to trust the GUN in the cert
|
||||
|
@ -137,31 +163,9 @@ func keysList(cmd *cobra.Command, args []string) {
|
|||
|
||||
fmt.Println("")
|
||||
fmt.Println("# Signing keys: ")
|
||||
filepath.Walk(viper.GetString("privDir"), printAllPrivateKeys)
|
||||
}
|
||||
|
||||
func printAllPrivateKeys(fp string, fi os.FileInfo, err error) error {
|
||||
// If there are errors, ignore this particular file
|
||||
if err != nil {
|
||||
return nil
|
||||
for _, k := range privKeyStore.List() {
|
||||
printKey(k)
|
||||
}
|
||||
// Ignore if it is a directory
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
//TODO (diogo): make the key extension not be hardcoded
|
||||
// Only allow matches that end with our key extension .key
|
||||
matched, _ := filepath.Match("*.key", fi.Name())
|
||||
if matched {
|
||||
fp = strings.TrimSuffix(fp, filepath.Ext(fp))
|
||||
fp = strings.TrimPrefix(fp, viper.GetString("privDir"))
|
||||
|
||||
fingerprint := filepath.Base(fp)
|
||||
gun := filepath.Dir(fp)[1:]
|
||||
|
||||
fmt.Printf("%s %s\n", gun, fingerprint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func keysGenerate(cmd *cobra.Command, args []string) {
|
||||
|
@ -217,6 +221,15 @@ func printCert(cert *x509.Certificate) {
|
|||
fmt.Printf("%s %s (expires in: %v days)\n", cert.Subject.CommonName, string(subjectKeyID), math.Floor(timeDifference.Hours()/24))
|
||||
}
|
||||
|
||||
func printKey(keyPath string) {
|
||||
keyPath = strings.TrimSuffix(keyPath, filepath.Ext(keyPath))
|
||||
keyPath = strings.TrimPrefix(keyPath, viper.GetString("privDir"))
|
||||
|
||||
fingerprint := filepath.Base(keyPath)
|
||||
gun := filepath.Dir(keyPath)[1:]
|
||||
fmt.Printf("%s %s\n", gun, fingerprint)
|
||||
}
|
||||
|
||||
func askConfirm() bool {
|
||||
var res string
|
||||
_, err := fmt.Scanln(&res)
|
||||
|
|
|
@ -24,6 +24,8 @@ const privDir string = configPath + "private/"
|
|||
const tufDir string = configPath + "tuf/"
|
||||
|
||||
var caStore trustmanager.X509Store
|
||||
var privKeyStore trustmanager.FileStore
|
||||
|
||||
var rawOutput bool
|
||||
|
||||
func init() {
|
||||
|
@ -64,25 +66,24 @@ func init() {
|
|||
finalTrustDir := viper.GetString("trustDir")
|
||||
finalPrivDir := viper.GetString("privDir")
|
||||
|
||||
// Ensure the existence of the CAs directory
|
||||
err = trustmanager.CreateDirectory(finalTrustDir)
|
||||
if err != nil {
|
||||
fatalf("could not create directory: %v", err)
|
||||
}
|
||||
err = trustmanager.CreateDirectory(finalPrivDir)
|
||||
if err != nil {
|
||||
fatalf("could not create directory: %v", err)
|
||||
}
|
||||
|
||||
// Load all CAs that aren't expired and don't use SHA1
|
||||
// We could easily add "return cert.IsCA && cert.BasicConstraintsValid" in order
|
||||
// to have only valid CA certificates being loaded
|
||||
caStore = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool {
|
||||
caStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool {
|
||||
return time.Now().Before(cert.NotAfter) &&
|
||||
cert.SignatureAlgorithm != x509.SHA1WithRSA &&
|
||||
cert.SignatureAlgorithm != x509.DSAWithSHA1 &&
|
||||
cert.SignatureAlgorithm != x509.ECDSAWithSHA1
|
||||
})
|
||||
if err != nil {
|
||||
fatalf("could not create X509FileStore: %v", err)
|
||||
}
|
||||
|
||||
privKeyStore, err = trustmanager.NewPrivateFileStore(finalPrivDir, "key")
|
||||
if err != nil {
|
||||
fatalf("could not create FileStore: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
@ -410,7 +410,7 @@ func verify(cmd *cobra.Command, args []string) {
|
|||
stdinHash := fmt.Sprintf("sha256:%x", sha256.Sum256(payload))
|
||||
serverHash := fmt.Sprintf("sha256:%s", meta.Hashes["sha256"])
|
||||
if stdinHash != serverHash {
|
||||
_, _ = os.Stderr.Write([]byte("Data not present in the trusted collection.\n"))
|
||||
_, _ = os.Stderr.Write([]byte("notary: Data not present in the trusted collection.\n"))
|
||||
os.Exit(1)
|
||||
} else {
|
||||
_, _ = os.Stdout.Write(payload)
|
||||
|
|
|
@ -0,0 +1,164 @@
|
|||
package trustmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const visible os.FileMode = 0755
|
||||
const private os.FileMode = 0700
|
||||
|
||||
// FileStore is the interface for all FileStores
|
||||
type FileStore interface {
|
||||
Add(fileName string, data []byte) error
|
||||
Remove(fileName string) error
|
||||
RemoveGUN(gun string) error
|
||||
GetData(fileName string) ([]byte, error)
|
||||
GetPath(fileName string) string
|
||||
List() []string
|
||||
ListGUN(gun string) []string
|
||||
}
|
||||
|
||||
// fileStore implements FileStore
|
||||
type fileStore struct {
|
||||
baseDir string
|
||||
fileExt string
|
||||
perms os.FileMode
|
||||
}
|
||||
|
||||
// NewFileStore creates a directory with 755 permissions
|
||||
func NewFileStore(baseDir string, fileExt string) (FileStore, error) {
|
||||
if err := CreateDirectory(baseDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileStore{
|
||||
baseDir: baseDir,
|
||||
fileExt: fileExt,
|
||||
perms: visible,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewPrivateFileStore creates a directory with 700 permissions
|
||||
func NewPrivateFileStore(baseDir string, fileExt string) (FileStore, error) {
|
||||
if err := CreatePrivateDirectory(baseDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &fileStore{
|
||||
baseDir: baseDir,
|
||||
fileExt: fileExt,
|
||||
perms: private,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Add writes data to a file with a given name
|
||||
func (f *fileStore) Add(name string, data []byte) error {
|
||||
filePath := f.genFilePath(name)
|
||||
createDirectory(filepath.Dir(filePath), f.perms)
|
||||
return ioutil.WriteFile(filePath, data, f.perms)
|
||||
}
|
||||
|
||||
// Remove removes a file identified by a name
|
||||
// TODO (diogo): We can get rid of RemoveGUN by merging with Remove
|
||||
func (f *fileStore) Remove(name string) error {
|
||||
filePath := f.genFilePath(name)
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
|
||||
// RemoveGUN removes a directory identified by the Global Unique Name
|
||||
func (f *fileStore) RemoveGUN(gun string) error {
|
||||
dirPath := filepath.Join(f.baseDir, gun)
|
||||
|
||||
// Check to see if file exists
|
||||
fi, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Check to see if it is a directory
|
||||
if !fi.IsDir() {
|
||||
return fmt.Errorf("GUN not found: %s", gun)
|
||||
}
|
||||
|
||||
return os.RemoveAll(dirPath)
|
||||
}
|
||||
|
||||
// GetData returns the data given a file name
|
||||
func (f *fileStore) GetData(name string) ([]byte, error) {
|
||||
filePath := f.genFilePath(name)
|
||||
data, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// GetPath returns the full final path of a file with a given name
|
||||
func (f *fileStore) GetPath(name string) string {
|
||||
return f.genFilePath(name)
|
||||
}
|
||||
|
||||
// List lists all the files inside of a store
|
||||
func (f *fileStore) List() []string {
|
||||
return f.list(f.baseDir)
|
||||
}
|
||||
|
||||
// ListGUN lists all the files inside of a directory identified by a Global Unique Name.
|
||||
// TODO (diogo): We can get rid of ListGUN by merging with List
|
||||
func (f *fileStore) ListGUN(gun string) []string {
|
||||
gunPath := filepath.Join(f.baseDir, gun)
|
||||
return f.list(gunPath)
|
||||
}
|
||||
|
||||
// listGUN lists all the files in a directory given a full path
|
||||
func (f *fileStore) list(path string) []string {
|
||||
files := make([]string, 0, 0)
|
||||
filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error {
|
||||
// If there are errors, ignore this particular file
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Ignore if it is a directory
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
// Only allow matches that end with our certificate extension (e.g. *.crt)
|
||||
matched, _ := filepath.Match("*"+f.fileExt, fi.Name())
|
||||
|
||||
if matched {
|
||||
files = append(files, fp)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return files
|
||||
}
|
||||
|
||||
// genFilePath returns the full path with extension given a file name
|
||||
func (f *fileStore) genFilePath(name string) string {
|
||||
fileName := fmt.Sprintf("%s.%s", name, f.fileExt)
|
||||
return filepath.Join(f.baseDir, fileName)
|
||||
}
|
||||
|
||||
// CreateDirectory uses createDirectory to create a chmod 755 Directory
|
||||
func CreateDirectory(dir string) error {
|
||||
return createDirectory(dir, visible)
|
||||
}
|
||||
|
||||
// CreatePrivateDirectory uses createDirectory to create a chmod 700 Directory
|
||||
func CreatePrivateDirectory(dir string) error {
|
||||
return createDirectory(dir, private)
|
||||
}
|
||||
|
||||
// createDirectory receives a string of the path to a directory.
|
||||
// It does not support passing files, so the caller has to remove
|
||||
// the filename by doing filepath.Dir(full_path_to_file)
|
||||
func createDirectory(dir string, perms os.FileMode) error {
|
||||
// This prevents someone passing /path/to/dir and 'dir' not being created
|
||||
// If two '//' exist, MkdirAll deals it with correctly
|
||||
dir = dir + "/"
|
||||
return os.MkdirAll(dir, perms)
|
||||
}
|
|
@ -0,0 +1,344 @@
|
|||
package trustmanager
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddFile(t *testing.T) {
|
||||
testData := []byte("This test data should be part of the file.")
|
||||
testName := "docker.com/notary/certificate"
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
// Call the Add function
|
||||
err = store.Add(testName, testData)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to add file to store: %v", err)
|
||||
}
|
||||
|
||||
// Check to see if file exists
|
||||
b, err := ioutil.ReadFile(expectedFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected file not found: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(b, testData) {
|
||||
t.Fatalf("unexpected content in the file: %s", expectedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFile(t *testing.T) {
|
||||
testName := "docker.com/notary/certificate"
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
|
||||
|
||||
_, err = generateRandomFile(expectedFilePath, perms)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate random file: %v", err)
|
||||
}
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
// Call the Remove function
|
||||
err = store.Remove(testName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove file from store: %v", err)
|
||||
}
|
||||
|
||||
// Check to see if file exists
|
||||
_, err = os.Stat(expectedFilePath)
|
||||
if err == nil {
|
||||
t.Fatalf("expected not to find file: %s", expectedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveGUN(t *testing.T) {
|
||||
testName := "docker.com/diogomonica/"
|
||||
testExt := "key"
|
||||
perms := os.FileMode(0700)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
|
||||
|
||||
_, err = generateRandomFile(expectedFilePath, perms)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate random file: %v", err)
|
||||
}
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
// Call the RemoveGUN function
|
||||
err = store.RemoveGUN(testName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to remove directory: %v", err)
|
||||
}
|
||||
|
||||
expectedDirectory := filepath.Dir(expectedFilePath)
|
||||
// Check to see if file exists
|
||||
_, err = os.Stat(expectedDirectory)
|
||||
if err == nil {
|
||||
t.Fatalf("expected not to find directory: %s", expectedDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
testName := "docker.com/notary/certificate"
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
var expectedFilePath string
|
||||
// Create 10 randomfiles
|
||||
for i := 1; i <= 10; i++ {
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
expectedFilePath = filepath.Join(tempBaseDir, testName+string(i)+"."+testExt)
|
||||
_, err = generateRandomFile(expectedFilePath, perms)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate random file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
// Call the List function
|
||||
files := store.List()
|
||||
if len(files) != 10 {
|
||||
t.Fatalf("expected 10 files in listing, got: %d", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGUN(t *testing.T) {
|
||||
testName := "docker.com/notary/certificate"
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
var expectedFilePath string
|
||||
// Create 10 randomfiles
|
||||
for i := 1; i <= 10; i++ {
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
fileName := fmt.Sprintf("%s-%s.%s", testName, strconv.Itoa(i), testExt)
|
||||
expectedFilePath = filepath.Join(tempBaseDir, fileName)
|
||||
fmt.Println(expectedFilePath)
|
||||
_, err = generateRandomFile(expectedFilePath, perms)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate random file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
// Call the ListGUN function
|
||||
files := store.ListGUN("docker.com/")
|
||||
if len(files) != 10 {
|
||||
t.Fatalf("expected 10 files in listing, got: %d", len(files))
|
||||
}
|
||||
files = store.ListGUN("docker.com/notary")
|
||||
if len(files) != 10 {
|
||||
t.Fatalf("expected 10 files in listing, got: %d", len(files))
|
||||
}
|
||||
files = store.ListGUN("fakedocker.com/")
|
||||
if len(files) != 0 {
|
||||
t.Fatalf("expected 0 files in listing, got: %d", len(files))
|
||||
}
|
||||
}
|
||||
func TestGetPath(t *testing.T) {
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: "",
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
|
||||
firstPath := "diogomonica.com/openvpn/0xdeadbeef.crt"
|
||||
secondPath := "/docker.io/testing-dashes/@#$%^&().crt"
|
||||
if store.GetPath("diogomonica.com/openvpn/0xdeadbeef") != firstPath {
|
||||
t.Fatalf("Expecting: %s", firstPath)
|
||||
}
|
||||
if store.GetPath("/docker.io/testing-dashes/@#$%^&()") != secondPath {
|
||||
t.Fatalf("Expecting: %s", secondPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetData(t *testing.T) {
|
||||
testName := "docker.com/notary/certificate"
|
||||
testExt := "crt"
|
||||
perms := os.FileMode(0755)
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
// Since we're generating this manually we need to add the extension '.'
|
||||
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
|
||||
|
||||
expectedData, err := generateRandomFile(expectedFilePath, perms)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate random file: %v", err)
|
||||
}
|
||||
|
||||
// Create our FileStore
|
||||
store := &fileStore{
|
||||
baseDir: tempBaseDir,
|
||||
fileExt: testExt,
|
||||
perms: perms,
|
||||
}
|
||||
testData, err := store.GetData(testName)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get data from: %s", testName)
|
||||
|
||||
}
|
||||
if !bytes.Equal(testData, expectedData) {
|
||||
t.Fatalf("unexpected content for the file: %s", expectedFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDirectory(t *testing.T) {
|
||||
testDir := "fake/path/to/directory"
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
dirPath := filepath.Join(tempBaseDir, testDir)
|
||||
|
||||
// Call createDirectory
|
||||
CreateDirectory(dirPath)
|
||||
|
||||
// Check to see if file exists
|
||||
fi, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected find directory: %s", dirPath)
|
||||
}
|
||||
|
||||
// Check to see if it is a directory
|
||||
if !fi.IsDir() {
|
||||
t.Fatalf("expected to be directory: %s", dirPath)
|
||||
}
|
||||
|
||||
// Check to see if the permissions match
|
||||
if fi.Mode().String() != "drwxr-xr-x" {
|
||||
t.Fatalf("permissions are wrong for: %s. Got: %s", dirPath, fi.Mode().String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePrivateDirectory(t *testing.T) {
|
||||
testDir := "fake/path/to/private/directory"
|
||||
|
||||
// Temporary directory where test files will be created
|
||||
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create a temporary directory: %v", err)
|
||||
}
|
||||
|
||||
dirPath := filepath.Join(tempBaseDir, testDir)
|
||||
|
||||
// Call createDirectory
|
||||
CreatePrivateDirectory(dirPath)
|
||||
|
||||
// Check to see if file exists
|
||||
fi, err := os.Stat(dirPath)
|
||||
if err != nil {
|
||||
t.Fatalf("expected find directory: %s", dirPath)
|
||||
}
|
||||
|
||||
// Check to see if it is a directory
|
||||
if !fi.IsDir() {
|
||||
t.Fatalf("expected to be directory: %s", dirPath)
|
||||
}
|
||||
|
||||
// Check to see if the permissions match
|
||||
if fi.Mode().String() != "drwx------" {
|
||||
t.Fatalf("permissions are wrong for: %s. Got: %s", dirPath, fi.Mode().String())
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomFile(filePath string, perms os.FileMode) ([]byte, error) {
|
||||
rndBytes := make([]byte, 10)
|
||||
_, err := rand.Read(rndBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
os.MkdirAll(filepath.Dir(filePath), perms)
|
||||
if err = ioutil.WriteFile(filePath, rndBytes, perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rndBytes, nil
|
||||
}
|
|
@ -9,46 +9,42 @@ import (
|
|||
|
||||
// X509FileStore implements X509Store that persists on disk
|
||||
type X509FileStore struct {
|
||||
baseDir string
|
||||
validate Validator
|
||||
fileMap map[ID]string
|
||||
fingerprintMap map[ID]*x509.Certificate
|
||||
nameMap map[string][]ID
|
||||
fileMap map[CertID]string
|
||||
fingerprintMap map[CertID]*x509.Certificate
|
||||
nameMap map[string][]CertID
|
||||
fileStore FileStore
|
||||
}
|
||||
|
||||
// NewX509FileStore returns a new X509FileStore.
|
||||
func NewX509FileStore(directory string) *X509FileStore {
|
||||
func NewX509FileStore(directory string) (*X509FileStore, error) {
|
||||
validate := ValidatorFunc(func(cert *x509.Certificate) bool { return true })
|
||||
|
||||
s := &X509FileStore{
|
||||
|
||||
baseDir: directory,
|
||||
validate: validate,
|
||||
fileMap: make(map[ID]string),
|
||||
fingerprintMap: make(map[ID]*x509.Certificate),
|
||||
nameMap: make(map[string][]ID),
|
||||
}
|
||||
|
||||
loadCertsFromDir(s, directory)
|
||||
|
||||
return s
|
||||
return newX509FileStore(directory, validate)
|
||||
}
|
||||
|
||||
// NewX509FilteredFileStore returns a new X509FileStore that validates certificates
|
||||
// that are added.
|
||||
func NewX509FilteredFileStore(directory string, validate func(*x509.Certificate) bool) *X509FileStore {
|
||||
s := &X509FileStore{
|
||||
func NewX509FilteredFileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) {
|
||||
return newX509FileStore(directory, validate)
|
||||
}
|
||||
|
||||
baseDir: directory,
|
||||
validate: ValidatorFunc(validate),
|
||||
fileMap: make(map[ID]string),
|
||||
fingerprintMap: make(map[ID]*x509.Certificate),
|
||||
nameMap: make(map[string][]ID),
|
||||
func newX509FileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) {
|
||||
fileStore, err := NewFileStore(directory, certExtension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loadCertsFromDir(s, directory)
|
||||
s := &X509FileStore{
|
||||
validate: ValidatorFunc(validate),
|
||||
fileMap: make(map[CertID]string),
|
||||
fingerprintMap: make(map[CertID]*x509.Certificate),
|
||||
nameMap: make(map[string][]CertID),
|
||||
fileStore: fileStore,
|
||||
}
|
||||
|
||||
return s
|
||||
loadCertsFromDir(s)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// AddCert creates a filename for a given cert and adds a certificate with that name
|
||||
|
@ -57,11 +53,12 @@ func (s X509FileStore) AddCert(cert *x509.Certificate) error {
|
|||
return errors.New("adding nil Certificate to X509Store")
|
||||
}
|
||||
|
||||
var filename string
|
||||
fingerprint := string(FingerprintCert(cert))
|
||||
filename = path.Join(s.baseDir, cert.Subject.CommonName, fingerprint+certExtension)
|
||||
|
||||
if err := s.addNamedCert(cert, filename); err != nil {
|
||||
// Check if this certificate meets our validation criteria
|
||||
if !s.validate.Validate(cert) {
|
||||
return errors.New("certificate validation failed")
|
||||
}
|
||||
// Attempt to write the certificate to the file
|
||||
if err := s.addNamedCert(cert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -70,11 +67,7 @@ func (s X509FileStore) AddCert(cert *x509.Certificate) error {
|
|||
|
||||
// addNamedCert allows adding a certificate while controling the filename it gets
|
||||
// stored under. If the file does not exist on disk, saves it.
|
||||
func (s X509FileStore) addNamedCert(cert *x509.Certificate, filename string) error {
|
||||
if cert == nil {
|
||||
return errors.New("adding nil Certificate to X509Store")
|
||||
}
|
||||
|
||||
func (s X509FileStore) addNamedCert(cert *x509.Certificate) error {
|
||||
fingerprint := FingerprintCert(cert)
|
||||
|
||||
// Validate if we already loaded this certificate before
|
||||
|
@ -82,23 +75,25 @@ func (s X509FileStore) addNamedCert(cert *x509.Certificate, filename string) err
|
|||
return errors.New("certificate already in the store")
|
||||
}
|
||||
|
||||
// Check if this certificate meets our validation criteria
|
||||
if !s.validate.Validate(cert) {
|
||||
return errors.New("certificate validation failed")
|
||||
// Convert certificate to PEM
|
||||
certBytes := ToPEM(cert)
|
||||
// Compute FileName
|
||||
fileName := fileName(cert)
|
||||
|
||||
// Save the file to disk if not already there.
|
||||
if _, err := os.Stat(fileName); os.IsNotExist(err) {
|
||||
if err := s.fileStore.Add(fileName, certBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add the certificate to our in-memory storage
|
||||
// We wrote the certificate succcessfully, add it to our in-memory storage
|
||||
s.fingerprintMap[fingerprint] = cert
|
||||
s.fileMap[fingerprint] = filename
|
||||
s.fileMap[fingerprint] = fileName
|
||||
|
||||
name := string(cert.RawSubject)
|
||||
s.nameMap[name] = append(s.nameMap[name], fingerprint)
|
||||
|
||||
// Save the file to disk if not already there.
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
return saveCertificate(cert, filename)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -126,7 +121,7 @@ func (s X509FileStore) RemoveCert(cert *x509.Certificate) error {
|
|||
|
||||
s.nameMap[name] = newfpList
|
||||
|
||||
if err := os.Remove(filename); err != nil {
|
||||
if err := s.fileStore.Remove(filename); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -183,7 +178,7 @@ func (s X509FileStore) GetCertificateBykID(hexkID string) (*x509.Certificate, er
|
|||
}
|
||||
|
||||
// Check to see if this subject key identifier exists
|
||||
if cert, ok := s.fingerprintMap[ID(hexkID)]; ok {
|
||||
if cert, ok := s.fingerprintMap[CertID(hexkID)]; ok {
|
||||
return cert, nil
|
||||
|
||||
}
|
||||
|
@ -207,3 +202,7 @@ func (s X509FileStore) GetVerifyOptions(dnsName string) (x509.VerifyOptions, err
|
|||
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func fileName(cert *x509.Certificate) string {
|
||||
return path.Join(cert.Subject.CommonName, string(FingerprintCert(cert)))
|
||||
}
|
|
@ -9,8 +9,8 @@ import (
|
|||
// X509MemStore implements X509Store as an in-memory object with no persistence
|
||||
type X509MemStore struct {
|
||||
validate Validator
|
||||
fingerprintMap map[ID]*x509.Certificate
|
||||
nameMap map[string][]ID
|
||||
fingerprintMap map[CertID]*x509.Certificate
|
||||
nameMap map[string][]CertID
|
||||
}
|
||||
|
||||
// NewX509MemStore returns a new X509MemStore.
|
||||
|
@ -19,8 +19,8 @@ func NewX509MemStore() *X509MemStore {
|
|||
|
||||
return &X509MemStore{
|
||||
validate: validate,
|
||||
fingerprintMap: make(map[ID]*x509.Certificate),
|
||||
nameMap: make(map[string][]ID),
|
||||
fingerprintMap: make(map[CertID]*x509.Certificate),
|
||||
nameMap: make(map[string][]CertID),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,8 @@ func NewX509FilteredMemStore(validate func(*x509.Certificate) bool) *X509MemStor
|
|||
s := &X509MemStore{
|
||||
|
||||
validate: ValidatorFunc(validate),
|
||||
fingerprintMap: make(map[ID]*x509.Certificate),
|
||||
nameMap: make(map[string][]ID),
|
||||
fingerprintMap: make(map[CertID]*x509.Certificate),
|
||||
nameMap: make(map[string][]CertID),
|
||||
}
|
||||
|
||||
return s
|
||||
|
@ -147,7 +147,7 @@ func (s X509MemStore) GetCertificateBykID(hexkID string) (*x509.Certificate, err
|
|||
}
|
||||
|
||||
// Check to see if this subject key identifier exists
|
||||
if cert, ok := s.fingerprintMap[ID(hexkID)]; ok {
|
||||
if cert, ok := s.fingerprintMap[CertID(hexkID)]; ok {
|
||||
return cert, nil
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@ package trustmanager
|
|||
|
||||
import "crypto/x509"
|
||||
|
||||
const certExtension string = ".crt"
|
||||
const certExtension string = "crt"
|
||||
|
||||
// X509Store is the interface for all X509Stores
|
||||
type X509Store interface {
|
||||
|
@ -16,7 +16,7 @@ type X509Store interface {
|
|||
GetVerifyOptions(dnsName string) (x509.VerifyOptions, error)
|
||||
}
|
||||
|
||||
type ID string
|
||||
type CertID string
|
||||
|
||||
// Validator is a convenience type to create validating function that filters
|
||||
// certificates that get added to the store
|
|
@ -4,12 +4,9 @@ import (
|
|||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/endophage/gotuf/data"
|
||||
)
|
||||
|
@ -48,76 +45,18 @@ func GetCertFromURL(urlStr string) (*x509.Certificate, error) {
|
|||
return cert, nil
|
||||
}
|
||||
|
||||
// saveCertificate is an utility function that saves a certificate as a PEM
|
||||
// encoded block to a file.
|
||||
func saveCertificate(cert *x509.Certificate, filename string) error {
|
||||
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||
pemdata := string(pem.EncodeToMemory(&block))
|
||||
// ToPEM is an utility function returns a PEM encoded x509 Certificate
|
||||
func ToPEM(cert *x509.Certificate) []byte {
|
||||
pemCert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
|
||||
|
||||
err := CreateDirectory(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = ioutil.WriteFile(filename, []byte(pemdata), 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return pemCert
|
||||
}
|
||||
|
||||
func FingerprintCert(cert *x509.Certificate) ID {
|
||||
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||
pemdata := string(pem.EncodeToMemory(&block))
|
||||
// TeyToPEM is an utility function returns a PEM encoded Key
|
||||
func KeyToPEM(keyBytes []byte) []byte {
|
||||
keyPEMBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
|
||||
|
||||
// Create new TUF Key so we can compute the TUF-compliant ID
|
||||
tufKey := data.NewTUFKey("RSA", pemdata, "")
|
||||
|
||||
return ID(tufKey.ID())
|
||||
}
|
||||
|
||||
// loadCertsFromDir receives a store and a directory and calls AddCertFromFile
|
||||
// for each certificate found
|
||||
func loadCertsFromDir(s *X509FileStore, directory string) {
|
||||
filepath.Walk(directory, func(fp string, fi os.FileInfo, err error) error {
|
||||
// If there are errors, ignore this particular file
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// Ignore if it is a directory
|
||||
if !!fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
// Only allow matches that end with our certificate extension (e.g. *.crt)
|
||||
matched, _ := filepath.Match("*"+certExtension, fi.Name())
|
||||
|
||||
if matched {
|
||||
s.AddCertFromFile(fp)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// LoadCertFromFile tries to adds a X509 certificate to the store given a filename
|
||||
func LoadCertFromFile(filename string) (*x509.Certificate, error) {
|
||||
// TODO(diogo): handle multiple certificates in one file. Demultiplex into
|
||||
// multiple files or load only first
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var block *pem.Block
|
||||
block, b = pem.Decode(b)
|
||||
for ; block != nil; block, b = pem.Decode(b) {
|
||||
if block.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("could not load certificate from file")
|
||||
return keyPEMBytes
|
||||
}
|
||||
|
||||
// loadCertFromPEM returns the first certificate found in a bunch of bytes or error
|
||||
|
@ -144,10 +83,41 @@ func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) {
|
|||
return nil, errors.New("no certificates found in PEM data")
|
||||
}
|
||||
|
||||
func CreateDirectory(dir string) error {
|
||||
cleanDir := filepath.Dir(dir)
|
||||
if err := os.MkdirAll(cleanDir, 0700); err != nil {
|
||||
return fmt.Errorf("cannot create directory: %v", err)
|
||||
}
|
||||
return nil
|
||||
func FingerprintCert(cert *x509.Certificate) CertID {
|
||||
block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}
|
||||
pemdata := string(pem.EncodeToMemory(&block))
|
||||
|
||||
// Create new TUF Key so we can compute the TUF-compliant CertID
|
||||
tufKey := data.NewTUFKey("RSA", pemdata, "")
|
||||
|
||||
return CertID(tufKey.ID())
|
||||
}
|
||||
|
||||
// loadCertsFromDir receives a store AddCertFromFile for each certificate found
|
||||
func loadCertsFromDir(s *X509FileStore) {
|
||||
certFiles := s.fileStore.List()
|
||||
for _, c := range certFiles {
|
||||
s.AddCertFromFile(c)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadCertFromFile tries to adds a X509 certificate to the store given a filename
|
||||
func LoadCertFromFile(filename string) (*x509.Certificate, error) {
|
||||
// TODO(diogo): handle multiple certificates in one file.
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var block *pem.Block
|
||||
block, b = pem.Decode(b)
|
||||
for ; block != nil; block, b = pem.Decode(b) {
|
||||
if block.Type == "CERTIFICATE" {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("could not load certificate from file")
|
||||
}
|
Loading…
Reference in New Issue