Merge pull request #12 from docker/filestore-refactor

Major refactor the the FileStore
This commit is contained in:
David Lawrence 2015-06-21 15:45:24 -07:00
commit 5efc8c0549
11 changed files with 669 additions and 199 deletions

View File

@ -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
}

View File

@ -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)

View File

@ -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() {

View File

@ -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)

164
trustmanager/filestore.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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)))
}

View File

@ -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
}

View File

@ -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

View File

@ -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")
}