Initial libnotary refactor

Signed-off-by: Diogo Monica <diogo@docker.com>

Ported more functionality to libnotary
This commit is contained in:
Diogo Monica 2015-07-03 16:42:21 -07:00
parent ce0ed53fa2
commit 1346296869
6 changed files with 723 additions and 445 deletions

View File

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

596
client/client.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

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