Added trustmanager package and simple CLI

This commit is contained in:
Diogo Monica 2015-05-30 00:30:47 -07:00 committed by David Lawrence
parent 00a4ef9d15
commit 6ffe6df102
9 changed files with 767 additions and 2 deletions

View File

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

46
cmd/trustmanager/list.go Normal file
View File

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

76
cmd/trustmanager/main.go Normal file
View File

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

42
cmd/trustmanager/trust.go Normal file
View File

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

View File

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

View File

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

View File

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

34
trustmanager/X509Store.go Normal file
View File

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

90
trustmanager/X509Utils.go Normal file
View File

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