From 6ffe6df1026592de906576eebf40c799a547e3f6 Mon Sep 17 00:00:00 2001 From: Diogo Monica Date: Sat, 30 May 2015 00:30:47 -0700 Subject: [PATCH] Added trustmanager package and simple CLI --- Makefile | 8 +- cmd/trustmanager/list.go | 46 ++++++ cmd/trustmanager/main.go | 76 ++++++++++ cmd/trustmanager/trust.go | 42 ++++++ cmd/trustmanager/untrust.go | 38 +++++ trustmanager/X509FileStore.go | 256 ++++++++++++++++++++++++++++++++++ trustmanager/X509MemStore.go | 179 ++++++++++++++++++++++++ trustmanager/X509Store.go | 34 +++++ trustmanager/X509Utils.go | 90 ++++++++++++ 9 files changed, 767 insertions(+), 2 deletions(-) create mode 100644 cmd/trustmanager/list.go create mode 100644 cmd/trustmanager/main.go create mode 100644 cmd/trustmanager/trust.go create mode 100644 cmd/trustmanager/untrust.go create mode 100644 trustmanager/X509FileStore.go create mode 100644 trustmanager/X509MemStore.go create mode 100644 trustmanager/X509Store.go create mode 100644 trustmanager/X509Utils.go diff --git a/Makefile b/Makefile index 287442c884..ff365628fe 100644 --- a/Makefile +++ b/Makefile @@ -19,6 +19,10 @@ ${PREFIX}/bin/vetinari-server: version/version.go $(shell find . -type f -name ' @echo "+ $@" @go build -o $@ ${GO_LDFLAGS} ./cmd/vetinari-server +${PREFIX}/bin/trustmanager: version/version.go $(shell find . -type f -name '*.go') + @echo "+ $@" + @go build -o $@ ${GO_LDFLAGS} ./cmd/trustmanager + vet: @echo "+ $@" @test -z "$$(go tool vet -printf=false . 2>&1 | grep -v Godeps/_workspace/src/ | tee /dev/stderr)" @@ -49,9 +53,9 @@ protos: clean-protos: @rm proto/*.pb.go -binaries: ${PREFIX}/bin/vetinari-server +binaries: ${PREFIX}/bin/vetinari-server ${PREFIX}/bin/trustmanager @echo "+ $@" clean: @echo "+ $@" - @rm -rf "${PREFIX}/bin/vetinari-server" + @rm -rf "${PREFIX}/bin/vetinari-server" "${PREFIX}/bin/trustmanager" diff --git a/cmd/trustmanager/list.go b/cmd/trustmanager/list.go new file mode 100644 index 0000000000..383194fe05 --- /dev/null +++ b/cmd/trustmanager/list.go @@ -0,0 +1,46 @@ +package main + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/hex" + "fmt" + "math" + "time" + + "github.com/codegangsta/cli" +) + +var ( + commandList = cli.Command{ + Name: "list", + Usage: `List the currently trusted certificate authorities.`, + Description: `List the currently trusted certificate authorities.`, + Action: list, + } +) + +func list(ctx *cli.Context) { + // Load all the certificates + trustedCAs := caStore.GetCertificates() + trustedRepos := repoStore.GetCertificates() + + fmt.Println("CAs Loaded:") + for _, c := range trustedCAs { + print_cert(c) + } + + fmt.Println("Repos Loaded:") + for _, c := range trustedRepos { + print_cert(c) + } +} + +func print_cert(cert *x509.Certificate) { + timeDifference := cert.NotAfter.Sub(time.Now()) + fmt.Printf("Certificate: %s ; Expires in: %v days; SKID: %s\n", printPkix(cert.Subject), math.Floor(timeDifference.Hours()/24), hex.EncodeToString(cert.SubjectKeyId[:])) +} + +func printPkix(pkixName pkix.Name) string { + return fmt.Sprintf("%s - %s", pkixName.CommonName, pkixName.Organization) +} diff --git a/cmd/trustmanager/main.go b/cmd/trustmanager/main.go new file mode 100644 index 0000000000..31dcc7d4db --- /dev/null +++ b/cmd/trustmanager/main.go @@ -0,0 +1,76 @@ +package main + +import ( + "crypto/x509" + "fmt" + "os" + "os/user" + "path" + + "github.com/codegangsta/cli" + "github.com/docker/vetinari/trustmanager" +) + +const caDir string = ".docker/trust/certificate_authorities/" +const repoDir string = ".docker/trust/repositories/" + +var caStore trustmanager.X509Store +var repoStore trustmanager.X509Store + +func init() { + + // Retrieve current user to get home directory + usr, err := user.Current() + if err != nil { + errorf("cannot get current user: %v", err) + } + + // Get home directory for current user + homeDir := usr.HomeDir + if homeDir == "" { + errorf("cannot get current user home directory") + } + + // Ensure the existence of the CAs directory + fullCaDir, fullRepoDir := setupDefaultDirectories(homeDir) + + // TODO(diogo): inspect permissions of the directories/files. Warn. + caStore = trustmanager.NewX509FilteredFileStore(fullCaDir, func(cert *x509.Certificate) bool { + return cert.IsCA + }) + repoStore = trustmanager.NewX509FileStore(fullRepoDir) +} + +func main() { + app := cli.NewApp() + app.Name = "keymanager" + app.Usage = "trust keymanager" + + app.Commands = []cli.Command{ + commandTrust, + commandList, + commandUntrust, + } + + app.RunAndExitOnError() +} + +func errorf(format string, args ...interface{}) { + fmt.Printf("* fatal: "+format+"\n", args...) + os.Exit(1) +} + +func setupDefaultDirectories(homeDir string) (string, string) { + fullCaDir := path.Join(homeDir, path.Dir(caDir)) + if err := os.MkdirAll(fullCaDir, 0700); err != nil { + errorf("cannot create directory: %v", err) + } + + // Ensure the existence of the repositories directory + fullRepoDir := path.Join(homeDir, path.Dir(repoDir)) + if err := os.MkdirAll(fullRepoDir, 0700); err != nil { + errorf("cannot create directory: %v", err) + } + + return fullCaDir, fullRepoDir +} diff --git a/cmd/trustmanager/trust.go b/cmd/trustmanager/trust.go new file mode 100644 index 0000000000..68fb2f4be6 --- /dev/null +++ b/cmd/trustmanager/trust.go @@ -0,0 +1,42 @@ +package main + +import ( + "net/url" + "os" + + "github.com/codegangsta/cli" +) + +var ( + commandTrust = cli.Command{ + Name: "trust", + Usage: "Add an entry to the trusted certificate authority list.", + Description: "Add an entry to the trusted certificate authority list.", + Action: add, + } +) + +func add(ctx *cli.Context) { + args := []string(ctx.Args()) + + if len(args) < 1 { + cli.ShowCommandHelp(ctx, ctx.Command.Name) + errorf("must specify a URL or file.") + } + + // Verify if argument is a valid URL + url, err := url.Parse(args[0]) + if err == nil && url.Scheme != "" { + err = caStore.AddCertFromURL(args[0]) + if err != nil { + errorf("error adding certificate to CA Store: %v", err) + } + // Verify is argument is a valid file + } else if _, err := os.Stat(args[0]); err == nil { + if err := caStore.AddCertFromFile(args[0]); err != nil { + errorf("error adding certificate from file: %v", err) + } + } else { + errorf("please provide a file location or URL for CA certificate.") + } +} diff --git a/cmd/trustmanager/untrust.go b/cmd/trustmanager/untrust.go new file mode 100644 index 0000000000..d66d461aa1 --- /dev/null +++ b/cmd/trustmanager/untrust.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + + "github.com/codegangsta/cli" +) + +var ( + commandUntrust = cli.Command{ + Name: "untrust", + Usage: "remove trust from a specifice certificate authority", + Description: "remove trust from a specifice certificate authority.", + Action: untrust, + } +) + +func untrust(ctx *cli.Context) { + args := []string(ctx.Args()) + + if len(args) < 1 { + cli.ShowCommandHelp(ctx, ctx.Command.Name) + errorf("must specify a SHA256 SubjectKeyID of the certificate") + } + + cert, err := caStore.GetCertificateBySKID(args[0]) + if err != nil { + errorf("certificate not found") + } + + fmt.Printf("Removing: ") + print_cert(cert) + + err = caStore.RemoveCert(cert) + if err != nil { + errorf("failed to remove certificate for Key Store") + } +} diff --git a/trustmanager/X509FileStore.go b/trustmanager/X509FileStore.go new file mode 100644 index 0000000000..1dd0772cd4 --- /dev/null +++ b/trustmanager/X509FileStore.go @@ -0,0 +1,256 @@ +package trustmanager + +import ( + "crypto/sha256" + "crypto/x509" + "errors" + "io/ioutil" + "net/http" + "net/url" + "os" + "path" +) + +// 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 +} + +// NewX509FileStore returns a new X509FileStore. +func NewX509FileStore(directory string) *X509FileStore { + 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 +} + +// NewX509FilteredFileStore returns a new X509FileStore that validates certificates +// that are added. +func NewX509FilteredFileStore(directory string, validate func(*x509.Certificate) bool) *X509FileStore { + s := &X509FileStore{ + + baseDir: directory, + validate: ValidatorFunc(validate), + fileMap: make(map[ID]string), + fingerprintMap: make(map[ID]*x509.Certificate), + nameMap: make(map[string][]ID), + } + + loadCertsFromDir(s, directory) + + return s +} + +// AddCert creates a filename for a given cert and adds a certificate with that name +func (s X509FileStore) AddCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("adding nil Certificate to X509Store") + } + + fingerprint := fingerprintCert(cert) + filename := path.Join(s.baseDir, string(fingerprint)+certExtension) + if err := s.addNamedCert(cert, filename); err != nil { + return err + } + + return nil +} + +// 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") + } + + fingerprint := fingerprintCert(cert) + + // Validate if we already loaded this certificate before + if _, ok := s.fingerprintMap[fingerprint]; ok { + 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") + } + + // Overwrite every certificate SubjectKeyID with a SHA256 version. + subjectKeyID := sha256.Sum256(cert.Raw) + cert.SubjectKeyId = subjectKeyID[:] + + // Add the certificate to our in-memory storage + s.fingerprintMap[fingerprint] = cert + 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 +} + +// RemoveCert removes a certificate from a X509FileStore. +func (s X509FileStore) RemoveCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("removing nil Certificate from X509Store") + } + + fingerprint := fingerprintCert(cert) + delete(s.fingerprintMap, fingerprint) + filename := s.fileMap[fingerprint] + delete(s.fileMap, fingerprint) + + name := string(cert.RawSubject) + + // Filter the fingerprint out of this name entry + fpList := s.nameMap[name] + newfpList := fpList[:0] + for _, x := range fpList { + if x != fingerprint { + newfpList = append(newfpList, x) + } + } + + s.nameMap[name] = newfpList + + if err := os.Remove(filename); err != nil { + return err + } + + return nil +} + +// AddCertFromPEM adds the first certificate that it finds in the byte[], returning +// an error if no Certificates are found +func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { + cert, err := loadCertFromPEM(pemBytes) + if err != nil { + return err + } + return s.AddCert(cert) +} + +// AddCertFromFile tries to adds a X509 certificate to the store given a filename +func (s X509FileStore) AddCertFromFile(originFilname string) error { + cert, err := loadCertFromFile(originFilname) + if err != nil { + return err + } + + filename := s.genDestinationCertFilename(cert, originFilname) + + return s.addNamedCert(cert, filename) +} + +// AddCertFromURL tries to adds a X509 certificate to the store given a HTTPS URL +func (s X509FileStore) AddCertFromURL(urlStr string) error { + url, err := url.Parse(urlStr) + if err != nil { + return err + } + + // Check if we are adding via HTTPS + if url.Scheme != "https" { + return errors.New("only HTTPS URLs allowed.") + } + + // Download the certificate and write to directory + resp, err := http.Get(url.String()) + if err != nil { + return err + } + + // Copy the content to certBytes + defer resp.Body.Close() + certBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // Try to extract the first valid PEM certificate from the bytes + cert, err := loadCertFromPEM(certBytes) + if err != nil { + return err + } + + // Generate a unique destination URL based on the path + filename := s.genDestinationCertFilename(cert, url.Path) + + return s.addNamedCert(cert, filename) +} + +// GetCertificates returns an array with all of the current X509 Certificates. +func (s X509FileStore) GetCertificates() []*x509.Certificate { + certs := make([]*x509.Certificate, len(s.fingerprintMap)) + i := 0 + for _, v := range s.fingerprintMap { + certs[i] = v + i++ + } + return certs +} + +// GetCertificatePool returns an x509 CertPool loaded with all the certificates +// in the store. +func (s X509FileStore) GetCertificatePool() *x509.CertPool { + pool := x509.NewCertPool() + + for _, v := range s.fingerprintMap { + pool.AddCert(v) + } + return pool +} + +// genDestinationCertFilename generates a unique destination certificate filename +// given a sourceFilename to help keep indication to where the original file came from +func (s X509FileStore) genDestinationCertFilename(cert *x509.Certificate, sourceFilename string) string { + // Take the file name, extension and base name from filename + _, fName := path.Split(sourceFilename) + extName := path.Ext(sourceFilename) + bName := fName[:len(fName)-len(extName)] + + filename := path.Join(s.baseDir, bName+certExtension) + + // If a file with the same name already exists in the destination directory + // add hash to filename + if _, err := os.Stat(filename); err == nil { + fingerprint := fingerprintCert(cert) + // Add the certificate fingerprint to the file basename_FINGERPRINT.crt + filename = path.Join(s.baseDir, bName+"_"+string(fingerprint)+certExtension) + } + return filename +} + +// GetCertificateBySKID returns the certificate that matches a certain SKID or error +func (s X509FileStore) GetCertificateBySKID(hexSKID string) (*x509.Certificate, error) { + // If it does not look like a hex encoded sha256 hash, error + if len(hexSKID) != 64 { + return nil, errors.New("invalid Subject Key Identifier") + } + + // Check to see if this subject key identifier exists + if cert, ok := s.fingerprintMap[ID(hexSKID)]; ok { + return cert, nil + + } + return nil, errors.New("certificate not found in Key Store") +} diff --git a/trustmanager/X509MemStore.go b/trustmanager/X509MemStore.go new file mode 100644 index 0000000000..76b004edb7 --- /dev/null +++ b/trustmanager/X509MemStore.go @@ -0,0 +1,179 @@ +package trustmanager + +import ( + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "net/http" + "net/url" +) + +// 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 +} + +// NewX509MemStore returns a new X509MemStore. +func NewX509MemStore() *X509MemStore { + validate := ValidatorFunc(func(cert *x509.Certificate) bool { return true }) + + return &X509MemStore{ + validate: validate, + fingerprintMap: make(map[ID]*x509.Certificate), + nameMap: make(map[string][]ID), + } +} + +// AddCert adds a certificate to the store +func (s X509MemStore) AddCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("adding nil Certificate to X509Store") + } + + if !s.validate.Validate(cert) { + return errors.New("certificate failed validation") + } + + fingerprint := fingerprintCert(cert) + + s.fingerprintMap[fingerprint] = cert + name := string(cert.RawSubject) + s.nameMap[name] = append(s.nameMap[name], fingerprint) + + return nil +} + +// RemoveCert removes a certificate from a X509MemStore. +func (s X509MemStore) RemoveCert(cert *x509.Certificate) error { + if cert == nil { + return errors.New("removing nil Certificate to X509Store") + } + + fingerprint := fingerprintCert(cert) + delete(s.fingerprintMap, fingerprint) + name := string(cert.RawSubject) + + // Filter the fingerprint out of this name entry + fpList := s.nameMap[name] + newfpList := fpList[:0] + for _, x := range fpList { + if x != fingerprint { + newfpList = append(newfpList, x) + } + } + + s.nameMap[name] = newfpList + return nil +} + +// AddCertFromPEM adds a certificate to the store from a PEM blob +func (s X509MemStore) AddCertFromPEM(pemCerts []byte) error { + ok := false + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + return errors.New("no PEM data found") + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.New("error while parsing PEM certificate") + } + + s.AddCert(cert) + ok = true + } + + if !ok { + return errors.New("no certificates found in PEM data") + } + return nil +} + +// AddCertFromFile tries to adds a X509 certificate to the store given a filename +func (s X509MemStore) AddCertFromFile(originFilname string) error { + cert, err := loadCertFromFile(originFilname) + if err != nil { + return err + } + + return s.AddCert(cert) +} + +// AddCertFromURL tries to adds a X509 certificate to the store given a HTTPS URL +func (s X509MemStore) AddCertFromURL(urlStr string) error { + url, err := url.Parse(urlStr) + if err != nil { + return err + } + + // Check if we are adding via HTTPS + if url.Scheme != "https" { + return errors.New("only HTTPS URLs allowed.") + } + + // Download the certificate and write to directory + resp, err := http.Get(url.String()) + if err != nil { + return err + } + + // Copy the content to certBytes + defer resp.Body.Close() + certBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + // Try to extract the first valid PEM certificate from the bytes + cert, err := loadCertFromPEM(certBytes) + if err != nil { + return err + } + + return s.AddCert(cert) +} + +// GetCertificates returns an array with all of the current X509 Certificates. +func (s X509MemStore) GetCertificates() []*x509.Certificate { + certs := make([]*x509.Certificate, len(s.fingerprintMap)) + i := 0 + for _, v := range s.fingerprintMap { + certs[i] = v + i++ + } + return certs +} + +// GetCertificatePool returns an x509 CertPool loaded with all the certificates +// in the store. +func (s X509MemStore) GetCertificatePool() *x509.CertPool { + pool := x509.NewCertPool() + + for _, v := range s.fingerprintMap { + pool.AddCert(v) + } + return pool +} + +// GetCertificateBySKID returns the certificate that matches a certain SKID or error +func (s X509MemStore) GetCertificateBySKID(hexSKID string) (*x509.Certificate, error) { + // If it does not look like a hex encoded sha256 hash, error + if len(hexSKID) != 64 { + return nil, errors.New("invalid Subject Key Identifier") + } + + // Check to see if this subject key identifier exists + if cert, ok := s.fingerprintMap[ID(hexSKID)]; ok { + return cert, nil + + } + return nil, errors.New("certificate not found in Key Store") +} diff --git a/trustmanager/X509Store.go b/trustmanager/X509Store.go new file mode 100644 index 0000000000..60fd09f316 --- /dev/null +++ b/trustmanager/X509Store.go @@ -0,0 +1,34 @@ +package trustmanager + +import "crypto/x509" + +const certExtension string = ".crt" + +// X509Store is the interface for all X509Stores +type X509Store interface { + AddCert(cert *x509.Certificate) error + AddCertFromPEM(pemCerts []byte) error + AddCertFromFile(filename string) error + AddCertFromURL(urlStr string) error + RemoveCert(cert *x509.Certificate) error + GetCertificateBySKID(hexSKID string) (*x509.Certificate, error) + GetCertificates() []*x509.Certificate + GetCertificatePool() *x509.CertPool +} + +type ID string + +// Validator is a convenience type to create validating function +type Validator interface { + Validate(cert *x509.Certificate) bool +} + +// ValidatorFunc is a convenience type to create functions that implement +// the Validator interface +type ValidatorFunc func(cert *x509.Certificate) bool + +// Validate implements the Validator interface to allow for any func() bool method +// to be passed as a Validator +func (vf ValidatorFunc) Validate(cert *x509.Certificate) bool { + return vf(cert) +} diff --git a/trustmanager/X509Utils.go b/trustmanager/X509Utils.go new file mode 100644 index 0000000000..a9762979d8 --- /dev/null +++ b/trustmanager/X509Utils.go @@ -0,0 +1,90 @@ +package trustmanager + +import ( + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "path" + "path/filepath" +) + +// 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)) + + err := ioutil.WriteFile(filename, []byte(pemdata), 0600) + if err != nil { + return err + } + return nil +} + +func fingerprintCert(cert *x509.Certificate) ID { + fingerprintBytes := sha256.Sum256(cert.Raw) + return ID(hex.EncodeToString(fingerprintBytes[:])) +} + +// loadCertsFromDir receives a store and a directory and calls loadCertFromFile +// for each certificate found +func loadCertsFromDir(s *X509FileStore, directory string) { + certFiles, _ := filepath.Glob(path.Join(directory, fmt.Sprintf("*%s", certExtension))) + for _, f := range certFiles { + cert, err := loadCertFromFile(f) + // Ignores files that do not contain valid certificates + if err == nil { + s.addNamedCert(cert, f) + } + } +} + +// 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") +} + +// loadCertFromPEM returns the first certificate found in a bunch of bytes or error +// if nothing is found +func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { + for len(pemBytes) > 0 { + var block *pem.Block + block, pemBytes = pem.Decode(pemBytes) + if block == nil { + return nil, errors.New("no certificates found in PEM data") + } + if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + continue + } + + return cert, nil + } + + return nil, errors.New("no certificates found in PEM data") +}