diff --git a/cmd/notary/cli_crypto_service.go b/cmd/notary/cli_crypto_service.go index ef527b811d..e759b91be7 100644 --- a/cmd/notary/cli_crypto_service.go +++ b/cmd/notary/cli_crypto_service.go @@ -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 } diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 61c2c3a710..ba266a7808 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -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) diff --git a/cmd/notary/main.go b/cmd/notary/main.go index e67e78bf6b..25022c5e3e 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -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() { diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 2e8f6fc9e5..fbe368ac5c 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -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) diff --git a/trustmanager/filestore.go b/trustmanager/filestore.go new file mode 100644 index 0000000000..45808f3688 --- /dev/null +++ b/trustmanager/filestore.go @@ -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) +} diff --git a/trustmanager/filestore_test.go b/trustmanager/filestore_test.go new file mode 100644 index 0000000000..0b99f86bdc --- /dev/null +++ b/trustmanager/filestore_test.go @@ -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 +} diff --git a/trustmanager/X509FileStore.go b/trustmanager/x509filestore.go similarity index 74% rename from trustmanager/X509FileStore.go rename to trustmanager/x509filestore.go index 8cd9ba948c..3d03c304a0 100644 --- a/trustmanager/X509FileStore.go +++ b/trustmanager/x509filestore.go @@ -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))) +} diff --git a/trustmanager/X509MemStore.go b/trustmanager/x509memstore.go similarity index 92% rename from trustmanager/X509MemStore.go rename to trustmanager/x509memstore.go index 94c33ffb6c..d9c7e0923c 100644 --- a/trustmanager/X509MemStore.go +++ b/trustmanager/x509memstore.go @@ -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 } diff --git a/trustmanager/X509MemStore_test.go b/trustmanager/x509memstore_test.go similarity index 100% rename from trustmanager/X509MemStore_test.go rename to trustmanager/x509memstore_test.go diff --git a/trustmanager/X509Store.go b/trustmanager/x509store.go similarity index 95% rename from trustmanager/X509Store.go rename to trustmanager/x509store.go index 8e5763b621..b644a38789 100644 --- a/trustmanager/X509Store.go +++ b/trustmanager/x509store.go @@ -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 diff --git a/trustmanager/X509Utils.go b/trustmanager/x509utils.go similarity index 59% rename from trustmanager/X509Utils.go rename to trustmanager/x509utils.go index c4bfcee827..83627a25a3 100644 --- a/trustmanager/X509Utils.go +++ b/trustmanager/x509utils.go @@ -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") }