diff --git a/cmd/notary/cli_crypto_service.go b/client/cli_crypto_service.go similarity index 69% rename from cmd/notary/cli_crypto_service.go rename to client/cli_crypto_service.go index 3387e619b2..f54111fa1d 100644 --- a/cmd/notary/cli_crypto_service.go +++ b/client/cli_crypto_service.go @@ -1,4 +1,4 @@ -package main +package client import ( "crypto" @@ -8,41 +8,42 @@ import ( "crypto/x509" "encoding/pem" "fmt" - "io/ioutil" "path/filepath" "github.com/docker/notary/trustmanager" "github.com/endophage/gotuf/data" - "github.com/spf13/viper" ) type CliCryptoService struct { - privateKeys map[string]*data.PrivateKey - gun string + gun string + keyStore trustmanager.FileStore } // NewCryptoService returns an instance ofS cliCryptoService -func NewCryptoService(gun string) *CliCryptoService { - return &CliCryptoService{privateKeys: make(map[string]*data.PrivateKey), gun: gun} +func NewCryptoService(gun string, keyStore trustmanager.FileStore) *CliCryptoService { + return &CliCryptoService{gun: gun, keyStore: keyStore} } // Create is used to generate keys for targets, snapshots and timestamps func (ccs *CliCryptoService) Create(role string) (*data.PublicKey, error) { - _, cert, err := generateKeyAndCert(ccs.gun) + keyData, pemCert, err := GenerateKeyAndCert(ccs.gun) if err != nil { return nil, err } - // PEM ENcode the certificate, which will be put directly inside of TUF's root.json - block := pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw} - pemdata := pem.EncodeToMemory(&block) - - // If this key has the role root, save it as a trusted certificate on our certificateStore - if role == "root" { - certificateStore.AddCertFromPEM(pemdata) + fingerprint, err := trustmanager.FingerprintPEMCert(pemCert) + if err != nil { + return nil, err } - return data.NewPublicKey("RSA", pemdata), nil + // The key is going to be stored in the private directory, using the GUN and + // the filename will be the TUF-compliant ID. The Store takes care of extensions. + privKeyFilename := filepath.Join(ccs.gun, fingerprint) + + // Store this private key + ccs.keyStore.Add(privKeyFilename, keyData) + + return data.NewPublicKey("RSA", pemCert), nil } // Sign returns the signatures for data with the given keyIDs @@ -54,9 +55,9 @@ func (ccs *CliCryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signa signatures := make([]data.Signature, 0, len(keyIDs)) for _, fingerprint := range keyIDs { // Get the PrivateKey filename - privKeyFilename := filepath.Join(viper.GetString("privDir"), ccs.gun, fingerprint+".key") + privKeyFilename := filepath.Join(ccs.gun, fingerprint) // Read PrivateKey from file - privPEMBytes, err := ioutil.ReadFile(privKeyFilename) + privPEMBytes, err := ccs.keyStore.Get(privKeyFilename) if err != nil { continue } @@ -84,8 +85,9 @@ func (ccs *CliCryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signa return signatures, nil } -// generateKeyAndCert deals with the creation and storage of a key and returns a cert -func generateKeyAndCert(gun string) (crypto.PrivateKey, *x509.Certificate, error) { +// generateKeyAndCert deals with the creation and storage of a key and returns a +// PEM encoded cert +func GenerateKeyAndCert(gun string) ([]byte, []byte, error) { // Generates a new RSA key key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { @@ -96,7 +98,7 @@ func generateKeyAndCert(gun string) (crypto.PrivateKey, *x509.Certificate, error // TUF-compliant keyID //TODO (diogo): We're hardcoding the Organization to be the GUN. Probably want to // change it - template := newCertificate(gun, gun) + template := trustmanager.NewCertificate(gun, gun) derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key) if err != nil { return nil, nil, fmt.Errorf("failed to generate the certificate for key: %v", err) @@ -108,14 +110,10 @@ func generateKeyAndCert(gun string) (crypto.PrivateKey, *x509.Certificate, error return nil, nil, fmt.Errorf("failed to generate the certificate for key: %v", err) } - fingerprint := 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. The Store takes care of extensions. - privKeyFilename := filepath.Join(gun, fingerprint) pemKey, err := trustmanager.KeyToPEM(key) if err != nil { return nil, nil, fmt.Errorf("failed to generate the certificate for key: %v", err) } - return key, cert, privKeyStore.Add(privKeyFilename, pemKey) + return pemKey, trustmanager.CertToPEM(cert), nil } diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000000..88f908aa4e --- /dev/null +++ b/client/client.go @@ -0,0 +1,596 @@ +package client + +import ( + "bytes" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/docker/notary/trustmanager" + "github.com/endophage/gotuf" + tufclient "github.com/endophage/gotuf/client" + "github.com/endophage/gotuf/data" + "github.com/endophage/gotuf/keys" + "github.com/endophage/gotuf/signed" + "github.com/endophage/gotuf/store" + + "github.com/spf13/viper" +) + +// Default paths should end with a '/' so directory creation works correctly +const trustDir string = "/trusted_certificates/" +const privDir string = "/private/" +const tufDir string = "/tuf/" +const rootKeysDir string = "/root_keys/" + +// ErrRepositoryNotExist gets returned when trying to make an action over a repository +/// that doesn't exist +var ErrRepositoryNotExist = errors.New("repository does not exist") + +// Client is the interface that defines the Notary Client type +type Client interface { + ListPrivateKeys() []data.PrivateKey + GenRootKey(passphrase string) (*data.PublicKey, error) + GetRepository(gun string, baseURL string, transport http.RoundTripper) (Repository, error) +} + +// Repository is the interface that represents a Notary Repository +type Repository interface { + Update() error + Initialize(key *data.PublicKey) error + + AddTarget(target *Target) error + ListTargets() ([]*Target, error) + GetTargetByName(name string) (*Target, error) + + Publish() error +} + +type NotaryClient struct { + configFile string + caStore trustmanager.X509Store + certificateStore trustmanager.X509Store + rootKeyStore trustmanager.EncryptedFileStore +} + +type NotaryRepository struct { + Gun string + baseURL string + transport http.RoundTripper + signer *signed.Signer + tufRepo *tuf.TufRepo + fileStore store.MetadataStore + privKeyStore trustmanager.FileStore + caStore trustmanager.X509Store + certificateStore trustmanager.X509Store +} + +// Target represents a simplified version of the data TUF operates on. +type Target struct { + Name string + Hashes data.Hashes + Length int64 +} + +// NewTarget is a helper method that returns a Target +func NewTarget(targetName string, targetPath string) (*Target, error) { + b, err := ioutil.ReadFile(targetPath) + if err != nil { + return nil, err + } + + meta, err := data.NewFileMeta(bytes.NewBuffer(b)) + if err != nil { + return nil, err + } + + return &Target{Name: targetName, Hashes: meta.Hashes, Length: meta.Length}, nil +} + +// NewClient is a helper method that returns a new notary Client, given a config +// file. It makes the assumption that the base directory for the config file will +// be the place where trust information is being cached locally. +func NewClient(configFile string) (*NotaryClient, error) { + nClient := &NotaryClient{configFile: configFile} + err := nClient.loadKeys() + if err != nil { + return nil, err + } + + return nClient, nil +} + +// Update forces TUF to download the remote timestamps and verify if there are +// any remote changes. +func (r *NotaryRepository) Update() error { + return nil +} + +// Initialize creates a new repository by using rootKey as the root Key for the +// TUF repository. +func (r *NotaryRepository) Initialize(rootKey *data.PublicKey) error { + remote, err := getRemoteStore(r.Gun) + rawTSKey, err := remote.GetKey("timestamp") + if err != nil { + return err + } + fmt.Println("RawKey: ", string(rawTSKey)) + parsedKey := &data.TUFKey{} + err = json.Unmarshal(rawTSKey, parsedKey) + if err != nil { + return err + } + timestampKey := data.NewPublicKey(parsedKey.Cipher(), parsedKey.Public()) + + targetsKey, err := r.signer.Create("targets") + if err != nil { + return err + } + snapshotKey, err := r.signer.Create("snapshot") + if err != nil { + return err + } + + kdb := keys.NewDB() + + kdb.AddKey(rootKey) + kdb.AddKey(targetsKey) + kdb.AddKey(snapshotKey) + kdb.AddKey(timestampKey) + + rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil) + if err != nil { + return err + } + targetsRole, err := data.NewRole("targets", 1, []string{targetsKey.ID()}, nil, nil) + if err != nil { + return err + } + snapshotRole, err := data.NewRole("snapshot", 1, []string{snapshotKey.ID()}, nil, nil) + if err != nil { + return err + } + timestampRole, err := data.NewRole("timestamp", 1, []string{timestampKey.ID()}, nil, nil) + if err != nil { + return err + } + + err = kdb.AddRole(rootRole) + if err != nil { + return err + } + err = kdb.AddRole(targetsRole) + if err != nil { + return err + } + err = kdb.AddRole(snapshotRole) + if err != nil { + return err + } + err = kdb.AddRole(timestampRole) + if err != nil { + return err + } + + r.tufRepo = tuf.NewTufRepo(kdb, r.signer) + + r.fileStore, err = store.NewFilesystemStore( + path.Join(viper.GetString("tufDir")), + "metadata", + "json", + "targets", + ) + if err != nil { + return err + } + + err = r.tufRepo.InitRepo(false) + if err != nil { + return err + } + + r.saveRepo() + return nil +} + +// AddTarget adds a new target to the repository, forcing a timestamps check from TUF +func (r *NotaryRepository) AddTarget(target *Target) error { + r.bootstrapRepo() + + fmt.Printf("Adding target \"%s\" with sha256 \"%s\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length) + + meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes} + _, err := r.tufRepo.AddTargets("targets", data.Files{target.Name: meta}) + if err != nil { + return err + } + + r.saveRepo() + + return nil +} + +// ListTargets lists all targets for the current repository +func (r *NotaryRepository) ListTargets() ([]*Target, error) { + r.bootstrapRepo() + + c, err := r.bootstrapClient() + if err != nil { + return nil, err + } + + err = c.Update() + if err != nil { + return nil, err + } + + // TODO(diogo): return hashes + for name, meta := range r.tufRepo.Targets["targets"].Signed.Targets { + fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) + } + + return nil, nil +} + +// GetTargetByName returns a target given a name +func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) { + r.bootstrapRepo() + + c, err := r.bootstrapClient() + if err != nil { + return nil, err + } + + err = c.Update() + if err != nil { + return nil, err + } + + meta := c.TargetMeta(name) + if meta == nil { + return nil, errors.New("Meta is nil for target") + } + + return &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, nil +} + +// Publish pushes the local changes in signed material to the remote notary-server +func (r *NotaryRepository) Publish() error { + r.bootstrapRepo() + + remote, err := getRemoteStore(r.Gun) + + root, err := r.fileStore.GetMeta("root", 0) + if err != nil { + return err + } + targets, err := r.fileStore.GetMeta("targets", 0) + if err != nil { + return err + } + snapshot, err := r.fileStore.GetMeta("snapshot", 0) + if err != nil { + return err + } + + err = remote.SetMeta("root", root) + if err != nil { + return err + } + err = remote.SetMeta("targets", targets) + if err != nil { + return err + } + err = remote.SetMeta("snapshot", snapshot) + if err != nil { + return err + } + + return nil +} + +func (r *NotaryRepository) bootstrapRepo() error { + fileStore, err := store.NewFilesystemStore( + path.Join(viper.GetString("tufDir")), + "metadata", + "json", + "targets", + ) + if err != nil { + return err + } + + kdb := keys.NewDB() + tufRepo := tuf.NewTufRepo(kdb, r.signer) + + fmt.Println("Loading trusted collection.") + rootJSON, err := fileStore.GetMeta("root", 0) + if err != nil { + return err + } + root := &data.Signed{} + err = json.Unmarshal(rootJSON, root) + if err != nil { + return err + } + tufRepo.SetRoot(root) + targetsJSON, err := fileStore.GetMeta("targets", 0) + if err != nil { + return err + } + targets := &data.Signed{} + err = json.Unmarshal(targetsJSON, targets) + if err != nil { + return err + } + tufRepo.SetTargets("targets", targets) + snapshotJSON, err := fileStore.GetMeta("snapshot", 0) + if err != nil { + return err + } + snapshot := &data.Signed{} + err = json.Unmarshal(snapshotJSON, snapshot) + if err != nil { + return err + } + tufRepo.SetSnapshot(snapshot) + + r.tufRepo = tufRepo + r.fileStore = fileStore + + return nil +} + +func (r *NotaryRepository) saveRepo() error { + signedRoot, err := r.tufRepo.SignRoot(data.DefaultExpires("root")) + if err != nil { + return err + } + rootJSON, _ := json.Marshal(signedRoot) + r.fileStore.SetMeta("root", rootJSON) + + fmt.Println("Saving changes to Trusted Collection.") + + for t, _ := range r.tufRepo.Targets { + signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets")) + if err != nil { + return err + } + targetsJSON, _ := json.Marshal(signedTargets) + parentDir := filepath.Dir(t) + os.MkdirAll(parentDir, 0755) + r.fileStore.SetMeta(t, targetsJSON) + } + + signedSnapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot")) + if err != nil { + return err + } + snapshotJSON, _ := json.Marshal(signedSnapshot) + r.fileStore.SetMeta("snapshot", snapshotJSON) + + return nil +} + +/* +validateRoot iterates over every root key included in the TUF data and attempts +to validate the certificate by first checking for an exact match on the certificate +store, and subsequently trying to find a valid chain on the caStore. + +Example TUF Content for root role: +"roles" : { + "root" : { + "threshold" : 1, + "keyids" : [ + "e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" + ] + }, + ... +} + +Example TUF Content for root key: +"e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" : { + "keytype" : "RSA", + "keyval" : { + "private" : "", + "public" : "Base64-encoded, PEM encoded x509 Certificate" + } +} +*/ +func (r *NotaryRepository) ValidateRoot(root *data.Signed) error { + rootSigned := &data.Root{} + err := json.Unmarshal(root.Signed, rootSigned) + if err != nil { + return err + } + certs := make(map[string]*data.PublicKey) + for _, fingerprint := range rootSigned.Roles["root"].KeyIDs { + // TODO(dlaw): currently assuming only one cert contained in + // public key entry. Need to fix when we want to pass in chains. + k, _ := pem.Decode([]byte(rootSigned.Keys["kid"].Public())) + + decodedCerts, err := x509.ParseCertificates(k.Bytes) + if err != nil { + continue + } + + // TODO(diogo): Assuming that first certificate is the leaf-cert. Need to + // iterate over all decodedCerts and find a non-CA one (should be the last). + leafCert := decodedCerts[0] + leafID := trustmanager.FingerprintCert(leafCert) + + // Check to see if there is an exact match of this certificate. + // Checking the CommonName is not required since ID is calculated over + // Cert.Raw. It's included to prevent breaking logic with changes of how the + // ID gets computed. + _, err = r.certificateStore.GetCertificateByFingerprint(leafID) + if err == nil && leafCert.Subject.CommonName == r.Gun { + certs[fingerprint] = rootSigned.Keys[fingerprint] + } + + // Check to see if this leafCertificate has a chain to one of the Root CAs + // of our CA Store. + certList := []*x509.Certificate{leafCert} + err = trustmanager.Verify(r.caStore, r.Gun, certList) + if err == nil { + certs[fingerprint] = rootSigned.Keys[fingerprint] + } + } + _, err = signed.VerifyRoot(root, 0, certs, 1) + + return err +} + +func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) { + remote, err := getRemoteStore(r.Gun) + if err != nil { + return nil, err + } + + rootJSON, err := remote.GetMeta("root", 5<<20) + root := &data.Signed{} + err = json.Unmarshal(rootJSON, root) + if err != nil { + return nil, err + } + + err = r.ValidateRoot(root) + if err != nil { + return nil, err + } + err = r.tufRepo.SetRoot(root) + if err != nil { + return nil, err + } + + // TODO(dlaw): Where does this keyDB come in + kdb := keys.NewDB() + + return tufclient.NewClient( + r.tufRepo, + remote, + kdb, + ), nil +} + +// ListPrivateKeys lists all availables private keys. Does not include private key +// material +func (c *NotaryClient) ListPrivateKeys() []data.PrivateKey { + // TODO(diogo): Make this work + for _, k := range c.rootKeyStore.ListAll() { + fmt.Println(k) + } + return nil +} + +// GenRootKey generates a new root key protected by a given passphrase +func (c *NotaryClient) GenRootKey(passphrase string) (*data.PublicKey, error) { + // When generating a root key, passing in a GUN to put into the cert + // doesn't make sense since this key can be used for multiple distinct + // repositories. + pemKey, _, err := GenerateKeyAndCert("TUF root key") + if err != nil { + return nil, fmt.Errorf("could not generate private key: %v", err) + } + + c.rootKeyStore.AddEncrypted("root", pemKey, passphrase) + + return data.NewPublicKey("RSA", pemKey), nil +} + +// GetRepository returns a new repository +func (c *NotaryClient) GetRepository(gun string, baseURL string, transport http.RoundTripper) (*NotaryRepository, error) { + privKeyStore, err := trustmanager.NewKeyFileStore(viper.GetString("privDir")) + if err != nil { + return nil, err + } + + signer := signed.NewSigner(NewCryptoService(gun, privKeyStore)) + + return &NotaryRepository{Gun: gun, + baseURL: baseURL, + transport: transport, + signer: signer, + caStore: c.caStore, + certificateStore: c.certificateStore}, nil +} + +func (c *NotaryClient) loadKeys() error { + filename := filepath.Base(c.configFile) + ext := filepath.Ext(c.configFile) + basePath := filepath.Dir(c.configFile) + + viper.SetConfigType(strings.TrimPrefix(ext, ".")) + viper.SetConfigName(strings.TrimSuffix(filename, ext)) + viper.AddConfigPath(basePath) + + // Find and read the config file + err := viper.ReadInConfig() + if err != nil { + // Ignore if the configuration file doesn't exist, we can use the defaults + if !os.IsNotExist(err) { + return err + } + } + + // Set up the defaults for our config + viper.SetDefault("trustDir", path.Join(basePath, path.Dir(trustDir))) + viper.SetDefault("privDir", path.Join(basePath, path.Dir(privDir))) + viper.SetDefault("tufDir", path.Join(basePath, path.Dir(tufDir))) + viper.SetDefault("rootKeysDir", path.Join(basePath, path.Dir(rootKeysDir))) + + // Get the final value for the CA directory + finalTrustDir := viper.GetString("trustDir") + finalRootKeysDir := viper.GetString("rootKeysDir") + + // Load all CAs that aren't expired and don't use SHA1 + c.caStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool { + return cert.IsCA && cert.BasicConstraintsValid && cert.SubjectKeyId != nil && + time.Now().Before(cert.NotAfter) && + cert.SignatureAlgorithm != x509.SHA1WithRSA && + cert.SignatureAlgorithm != x509.DSAWithSHA1 && + cert.SignatureAlgorithm != x509.ECDSAWithSHA1 + }) + if err != nil { + return err + } + + // Load all individual (non-CA) certificates that aren't expired and don't use SHA1 + c.certificateStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool { + return !cert.IsCA && + time.Now().Before(cert.NotAfter) && + cert.SignatureAlgorithm != x509.SHA1WithRSA && + cert.SignatureAlgorithm != x509.DSAWithSHA1 && + cert.SignatureAlgorithm != x509.ECDSAWithSHA1 + }) + if err != nil { + return err + } + + c.rootKeyStore, err = trustmanager.NewKeyFileStore(finalRootKeysDir) + if err != nil { + return err + } + + return nil +} + +// Use this to initialize remote HTTPStores from the config settings +func getRemoteStore(gun string) (store.RemoteStore, error) { + return store.NewHTTPStore( + "https://notary:4443/v2/"+gun+"/_trust/tuf/", + "", + "json", + "", + "key", + ) +} diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index f8ec61bcf7..897ea2472f 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -1,12 +1,9 @@ package main import ( - "crypto/rand" "crypto/x509" - "crypto/x509/pkix" "fmt" "math" - "math/big" "net/url" "os" "path/filepath" @@ -207,39 +204,14 @@ func keysGenerate(cmd *cobra.Command, args []string) { fatalf("invalid Global Unique Name: %s", gun) } - _, cert, err := generateKeyAndCert(gun) - if err != nil { - fatalf("could not generate key: %v", err) - } + // _, cert, err := generateKeyAndCert(gun) + // if err != nil { + // fatalf("could not generate key: %v", err) + // } - certificateStore.AddCert(cert) - fingerprint := trustmanager.FingerprintCert(cert) - fmt.Println("Generated new keypair with ID: ", fingerprint) -} - -func newCertificate(gun, organization string) *x509.Certificate { - notBefore := time.Now() - notAfter := notBefore.Add(time.Hour * 24 * 365 * 2) - - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - fatalf("failed to generate serial number: %s", err) - } - - return &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{organization}, - CommonName: gun, - }, - NotBefore: notBefore, - NotAfter: notAfter, - - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - BasicConstraintsValid: true, - } + // certificateStore.AddCert(cert) + // fingerprint := trustmanager.FingerprintCert(cert) + // fmt.Println("Generated new keypair with ID: ", fingerprint) } func printCert(cert *x509.Certificate) { diff --git a/cmd/notary/main.go b/cmd/notary/main.go index a8bf9b61a1..f7f6895fc4 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -12,22 +12,20 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + notaryclient "github.com/docker/notary/client" "github.com/docker/notary/trustmanager" ) const configFileName string = "config" - -// Default paths should end with a '/' so directory creation works correctly const configPath string = ".docker/trust/" const trustDir string = configPath + "trusted_certificates/" const privDir string = configPath + "private/" -const tufDir string = configPath + "tuf/" - -var caStore trustmanager.X509Store -var certificateStore trustmanager.X509Store -var privKeyStore trustmanager.EncryptedFileStore var rawOutput bool +var nClient *notaryclient.NotaryClient +var caStore trustmanager.X509Store +var certificateStore trustmanager.X509Store +var privKeyStore trustmanager.FileStore func init() { logrus.SetLevel(logrus.DebugLevel) @@ -48,6 +46,7 @@ func init() { viper.SetConfigName(configFileName) viper.AddConfigPath(path.Join(homeDir, path.Dir(configPath))) viper.SetConfigType("json") + configFilePath := path.Join(homeDir, path.Dir(configPath)) + "/" + configFileName + ".json" // Find and read the config file err = viper.ReadInConfig() @@ -60,8 +59,6 @@ func init() { // Set up the defaults for our config viper.SetDefault("trustDir", path.Join(homeDir, path.Dir(trustDir))) - viper.SetDefault("privDir", path.Join(homeDir, path.Dir(privDir))) - viper.SetDefault("tufDir", path.Join(homeDir, path.Dir(tufDir))) // Get the final value for the CA directory finalTrustDir := viper.GetString("trustDir") @@ -79,7 +76,7 @@ func init() { fatalf("could not create X509FileStore: %v", err) } - // Load all individual (non-CA) certificates that aren't expired and don't use SHA1 + // Load all individual (nonCA) certificates that aren't expired and don't use SHA1 certificateStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool { return !cert.IsCA && time.Now().Before(cert.NotAfter) && @@ -96,6 +93,12 @@ func init() { fatalf("could not create FileStore: %v", err) } + // TODO(diogo): Client should receive the config + nClient, err = notaryclient.NewClient(configFilePath) + if err != nil { + fatalf("could not create FileStore: %v", err) + } + } func main() { @@ -108,13 +111,13 @@ func main() { NotaryCmd.AddCommand(cmdKeys) NotaryCmd.AddCommand(cmdTufInit) NotaryCmd.AddCommand(cmdTufList) - cmdTufList.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary list to output a non-pretty printed version of the targets list. Useful if you need to parse the list.") + cmdTufList.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary list to output a nonpretty printed version of the targets list. Useful if you need to parse the list.") NotaryCmd.AddCommand(cmdTufAdd) NotaryCmd.AddCommand(cmdTufRemove) NotaryCmd.AddCommand(cmdTufPublish) cmdTufPublish.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location") NotaryCmd.AddCommand(cmdTufLookup) - cmdTufLookup.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary lookup to output a non-pretty printed version of the targets list. Useful if you need to parse the list.") + cmdTufLookup.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary lookup to output a nonpretty printed version of the targets list. Useful if you need to parse the list.") cmdTufLookup.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location") NotaryCmd.AddCommand(cmdVerify) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 958a0d68ae..8309a5ee12 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -1,27 +1,16 @@ package main import ( - "bytes" "crypto/sha256" - "crypto/x509" - "encoding/json" - "encoding/pem" "fmt" "io/ioutil" + "net/http" "os" - "path" - "path/filepath" "github.com/Sirupsen/logrus" - "github.com/docker/notary/trustmanager" - "github.com/endophage/gotuf" - "github.com/endophage/gotuf/client" - "github.com/endophage/gotuf/data" - "github.com/endophage/gotuf/keys" - "github.com/endophage/gotuf/signed" + notaryclient "github.com/docker/notary/client" "github.com/endophage/gotuf/store" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var remoteTrustServer string @@ -84,30 +73,19 @@ func tufAdd(cmd *cobra.Command, args []string) { gun := args[0] targetName := args[1] targetPath := args[2] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - repo := tuf.NewTufRepo(kdb, signer) - b, err := ioutil.ReadFile(targetPath) + t := &http.Transport{} + repo, err := nClient.GetRepository(gun, "", t) if err != nil { fatalf(err.Error()) } - filestore := bootstrapRepo(gun, repo) - - fmt.Println("Generating metadata for target") - meta, err := data.NewFileMeta(bytes.NewBuffer(b)) + target, err := notaryclient.NewTarget(targetName, targetPath) if err != nil { fatalf(err.Error()) } - - fmt.Printf("Adding target \"%s\" with sha256 \"%s\" and size %d bytes.\n", targetName, meta.Hashes["sha256"], meta.Length) - _, err = repo.AddTargets("targets", data.Files{targetName: meta}) - if err != nil { - fatalf(err.Error()) - } - - saveRepo(repo, filestore) + repo.AddTarget(target) + fmt.Println("Successfully added targets") } func tufInit(cmd *cobra.Command, args []string) { @@ -116,92 +94,19 @@ func tufInit(cmd *cobra.Command, args []string) { fatalf("Must specify a GUN") } - gun := args[0] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - - remote, err := getRemoteStore(gun) - rawTSKey, err := remote.GetKey("timestamp") - if err != nil { - fatalf(err.Error()) - } - fmt.Println("RawKey: ", string(rawTSKey)) - parsedKey := &data.TUFKey{} - err = json.Unmarshal(rawTSKey, parsedKey) - if err != nil { - fatalf(err.Error()) - } - timestampKey := data.NewPublicKey(parsedKey.Cipher(), parsedKey.Public()) - - rootKey, err := signer.Create("root") - if err != nil { - fatalf(err.Error()) - } - targetsKey, err := signer.Create("targets") - if err != nil { - fatalf(err.Error()) - } - snapshotKey, err := signer.Create("snapshot") + t := &http.Transport{} + repo, err := nClient.GetRepository(args[0], "", t) if err != nil { fatalf(err.Error()) } - kdb.AddKey(rootKey) - kdb.AddKey(targetsKey) - kdb.AddKey(snapshotKey) - kdb.AddKey(timestampKey) - - rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil) + // TODO(diogo): We don't want to generate a new root every time. Ask the user + // which key she wants to use if there > 0 root keys available. + newRootKey, err := nClient.GenRootKey("passphrase") if err != nil { fatalf(err.Error()) } - targetsRole, err := data.NewRole("targets", 1, []string{targetsKey.ID()}, nil, nil) - if err != nil { - fatalf(err.Error()) - } - snapshotRole, err := data.NewRole("snapshot", 1, []string{snapshotKey.ID()}, nil, nil) - if err != nil { - fatalf(err.Error()) - } - timestampRole, err := data.NewRole("timestamp", 1, []string{timestampKey.ID()}, nil, nil) - if err != nil { - fatalf(err.Error()) - } - - err = kdb.AddRole(rootRole) - if err != nil { - fatalf(err.Error()) - } - err = kdb.AddRole(targetsRole) - if err != nil { - fatalf(err.Error()) - } - err = kdb.AddRole(snapshotRole) - if err != nil { - fatalf(err.Error()) - } - err = kdb.AddRole(timestampRole) - if err != nil { - fatalf(err.Error()) - } - - repo := tuf.NewTufRepo(kdb, signer) - - filestore, err := store.NewFilesystemStore( - path.Join(viper.GetString("tufDir"), gun), // TODO: base trust dir from config - "metadata", - "json", - "targets", - ) - if err != nil { - fatalf(err.Error()) - } - - err = repo.InitRepo(false) - if err != nil { - fatalf(err.Error()) - } - saveRepo(repo, filestore) + repo.Initialize(newRootKey) } func tufList(cmd *cobra.Command, args []string) { @@ -210,32 +115,15 @@ func tufList(cmd *cobra.Command, args []string) { fatalf("must specify a GUN") } gun := args[0] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - remote, err := getRemoteStore(gun) + t := &http.Transport{} + repo, err := nClient.GetRepository(gun, "", t) if err != nil { - return - } - c, err := bootstrapClient(gun, remote, repo, kdb) - if err != nil { - return - } - err = c.Update() - if err != nil { - logrus.Error("Error updating client: ", err.Error()) - return + fatalf(err.Error()) } - if rawOutput { - for name, meta := range repo.Targets["targets"].Signed.Targets { - fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) - } - } else { - for name, meta := range repo.Targets["targets"].Signed.Targets { - fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) - } - } + // TODO(diogo): Parse Targets and print them + _, _ = repo.ListTargets() } func tufLookup(cmd *cobra.Command, args []string) { @@ -245,29 +133,20 @@ func tufLookup(cmd *cobra.Command, args []string) { } gun := args[0] targetName := args[1] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - remote, err := getRemoteStore(gun) - c, err := bootstrapClient(gun, remote, repo, kdb) + t := &http.Transport{} + repo, err := nClient.GetRepository(gun, "", t) if err != nil { - return + fatalf(err.Error()) } - err = c.Update() + + // TODO(diogo): Parse Targets and print them + target, err := repo.GetTargetByName(targetName) if err != nil { - logrus.Error("Error updating client: ", err.Error()) - return - } - meta := c.TargetMeta(targetName) - if meta == nil { - logrus.Infof("Target %s not found in %s.", targetName, gun) - return - } - if rawOutput { - fmt.Println(targetName, fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]), meta.Length) - } else { - fmt.Println(targetName, fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]), meta.Length) + fatalf(err.Error()) } + + fmt.Println(target.Name, fmt.Sprintf("sha256:%s", target.Hashes["sha256"]), target.Length) } func tufPublish(cmd *cobra.Command, args []string) { @@ -277,44 +156,16 @@ func tufPublish(cmd *cobra.Command, args []string) { } gun := args[0] + fmt.Println("Pushing changes to ", gun, ".") - remote, err := getRemoteStore(gun) - filestore, err := store.NewFilesystemStore( - path.Join(viper.GetString("tufDir"), gun), - "metadata", - "json", - "targets", - ) + t := &http.Transport{} + repo, err := nClient.GetRepository(gun, "", t) if err != nil { fatalf(err.Error()) } - root, err := filestore.GetMeta("root", 0) - if err != nil { - fatalf(err.Error()) - } - targets, err := filestore.GetMeta("targets", 0) - if err != nil { - fatalf(err.Error()) - } - snapshot, err := filestore.GetMeta("snapshot", 0) - if err != nil { - fatalf(err.Error()) - } - - err = remote.SetMeta("root", root) - if err != nil { - fatalf(err.Error()) - } - err = remote.SetMeta("targets", targets) - if err != nil { - fatalf(err.Error()) - } - err = remote.SetMeta("snapshot", snapshot) - if err != nil { - fatalf(err.Error()) - } + repo.Publish() } func tufRemove(cmd *cobra.Command, args []string) { @@ -324,20 +175,19 @@ func tufRemove(cmd *cobra.Command, args []string) { } gun := args[0] targetName := args[1] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - repo := tuf.NewTufRepo(kdb, signer) - fmt.Println("Removing target ", targetName, " from ", gun) - - filestore := bootstrapRepo(gun, repo) - - err := repo.RemoveTargets("targets", targetName) + t := &http.Transport{} + _, err := nClient.GetRepository(gun, "", t) if err != nil { fatalf(err.Error()) } - saveRepo(repo, filestore) + // TODO(diogo): Implement RemoveTargets in libnotary + fmt.Println("Removing target ", targetName, " from ", gun) + // repo.RemoveTargets("targets", targetName) + // if err != nil { + // fatalf(err.Error()) + // } } func verify(cmd *cobra.Command, args []string) { @@ -356,31 +206,22 @@ func verify(cmd *cobra.Command, args []string) { //TODO (diogo): This code is copy/pasted from lookup. gun := args[0] targetName := args[1] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - - remote, err := getRemoteStore(gun) - - c, err := bootstrapClient(gun, remote, repo, kdb) + t := &http.Transport{} + repo, err := nClient.GetRepository(gun, "", t) if err != nil { - logrus.Error("Unable to setup client.") - return - } - - err = c.Update() - if err != nil { - fmt.Println("Update failed") fatalf(err.Error()) } - meta := c.TargetMeta(targetName) - if meta == nil { + + // TODO(diogo): Parse Targets and print them + target, err := repo.GetTargetByName(targetName) + if err != nil { logrus.Error("notary: data not present in the trusted collection.") - os.Exit(1) + os.Exit(-11) } // Create hasher and hash data stdinHash := fmt.Sprintf("sha256:%x", sha256.Sum256(payload)) - serverHash := fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]) + serverHash := fmt.Sprintf("sha256:%s", target.Hashes["sha256"]) if stdinHash != serverHash { logrus.Error("notary: data not present in the trusted collection.") os.Exit(1) @@ -390,172 +231,6 @@ func verify(cmd *cobra.Command, args []string) { return } -func saveRepo(repo *tuf.TufRepo, filestore store.MetadataStore) error { - fmt.Println("Saving changes to Trusted Collection.") - signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) - if err != nil { - return err - } - rootJSON, _ := json.Marshal(signedRoot) - filestore.SetMeta("root", rootJSON) - - for r, _ := range repo.Targets { - signedTargets, err := repo.SignTargets(r, data.DefaultExpires("targets")) - if err != nil { - return err - } - targetsJSON, _ := json.Marshal(signedTargets) - parentDir := filepath.Dir(r) - os.MkdirAll(parentDir, 0755) - filestore.SetMeta(r, targetsJSON) - } - - signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) - if err != nil { - return err - } - snapshotJSON, _ := json.Marshal(signedSnapshot) - filestore.SetMeta("snapshot", snapshotJSON) - - return nil -} - -func bootstrapClient(gun string, remote store.RemoteStore, repo *tuf.TufRepo, kdb *keys.KeyDB) (*client.Client, error) { - rootJSON, err := remote.GetMeta("root", 5<<20) - root := &data.Signed{} - err = json.Unmarshal(rootJSON, root) - if err != nil { - return nil, err - } - err = validateRoot(gun, root) - if err != nil { - return nil, err - } - err = repo.SetRoot(root) - if err != nil { - return nil, err - } - return client.NewClient( - repo, - remote, - kdb, - ), nil -} - -/* -validateRoot iterates over every root key included in the TUF data and attempts -to validate the certificate by first checking for an exact match on the certificate -store, and subsequently trying to find a valid chain on the caStore. - -Example TUF Content for root role: -"roles" : { - "root" : { - "threshold" : 1, - "keyids" : [ - "e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" - ] - }, - ... -} - -Example TUF Content for root key: -"e6da5c303d572712a086e669ecd4df7b785adfc844e0c9a7b1f21a7dfc477a38" : { - "keytype" : "RSA", - "keyval" : { - "private" : "", - "public" : "Base64-encoded, PEM encoded x509 Certificate" - } -} -*/ -func validateRoot(gun string, root *data.Signed) error { - rootSigned := &data.Root{} - err := json.Unmarshal(root.Signed, rootSigned) - if err != nil { - return err - } - certs := make(map[string]*data.PublicKey) - for _, fingerprint := range rootSigned.Roles["root"].KeyIDs { - // TODO(dlaw): currently assuming only one cert contained in - // public key entry. Need to fix when we want to pass in chains. - k, _ := pem.Decode([]byte(rootSigned.Keys["kid"].Public())) - - decodedCerts, err := x509.ParseCertificates(k.Bytes) - if err != nil { - continue - } - - // TODO(diogo): Assuming that first certificate is the leaf-cert. Need to - // iterate over all decodedCerts and find a non-CA one (should be the last). - leafCert := decodedCerts[0] - leafID := trustmanager.FingerprintCert(leafCert) - - // Check to see if there is an exact match of this certificate. - // Checking the CommonName is not required since ID is calculated over - // Cert.Raw. It's included to prevent breaking logic with changes of how the - // ID gets computed. - _, err = certificateStore.GetCertificateByFingerprint(leafID) - if err == nil && leafCert.Subject.CommonName == gun { - certs[fingerprint] = rootSigned.Keys[fingerprint] - } - - // Check to see if this leafCertificate has a chain to one of the Root CAs - // of our CA Store. - certList := []*x509.Certificate{leafCert} - err = trustmanager.Verify(caStore, gun, certList) - if err == nil { - certs[fingerprint] = rootSigned.Keys[fingerprint] - } - } - _, err = signed.VerifyRoot(root, 0, certs, 1) - - return err -} - -func bootstrapRepo(gun string, repo *tuf.TufRepo) store.MetadataStore { - filestore, err := store.NewFilesystemStore( - path.Join(viper.GetString("tufDir"), gun), - "metadata", - "json", - "targets", - ) - if err != nil { - fatalf(err.Error()) - } - - fmt.Println("Loading trusted collection.") - rootJSON, err := filestore.GetMeta("root", 0) - if err != nil { - fatalf(err.Error()) - } - root := &data.Signed{} - err = json.Unmarshal(rootJSON, root) - if err != nil { - fatalf(err.Error()) - } - repo.SetRoot(root) - targetsJSON, err := filestore.GetMeta("targets", 0) - if err != nil { - fatalf(err.Error()) - } - targets := &data.Signed{} - err = json.Unmarshal(targetsJSON, targets) - if err != nil { - fatalf(err.Error()) - } - repo.SetTargets("targets", targets) - snapshotJSON, err := filestore.GetMeta("snapshot", 0) - if err != nil { - fatalf(err.Error()) - } - snapshot := &data.Signed{} - err = json.Unmarshal(snapshotJSON, snapshot) - if err != nil { - fatalf(err.Error()) - } - repo.SetSnapshot(snapshot) - return filestore -} - // Use this to initialize remote HTTPStores from the config settings func getRemoteStore(gun string) (store.RemoteStore, error) { return store.NewHTTPStore( diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index 9e2eed0c8d..fe5099cfe6 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -5,12 +5,15 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "crypto/x509/pkix" "encoding/pem" "errors" "fmt" "io/ioutil" + "math/big" "net/http" "net/url" + "time" "github.com/endophage/gotuf/data" ) @@ -117,6 +120,14 @@ func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { return nil, errors.New("no certificates found in PEM data") } +func FingerprintPEMCert(pemCert []byte) (string, error) { + cert, err := loadCertFromPEM(pemCert) + if err != nil { + return "", err + } + return FingerprintCert(cert), nil +} + // FingerprintCert returns a TUF compliant fingerprint for a X509 Certificate func FingerprintCert(cert *x509.Certificate) string { return string(fingerprintCert(cert)) @@ -215,3 +226,26 @@ func ParsePEMEncryptedPrivateKey(pemBytes []byte, passphrase string) (crypto.Pri return nil, fmt.Errorf("unsupported key type %q", block.Type) } } + +func NewCertificate(gun, organization string) *x509.Certificate { + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * 24 * 365 * 2) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + // TODO(diogo): Don't silently ignore this error + serialNumber, _ := rand.Int(rand.Reader, serialNumberLimit) + + return &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: gun, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + } +}