Merge pull request #37 from docker/tuf_rebase

Tuf rebase, ready to merge into master.
This commit is contained in:
Diogo Mónica 2015-07-09 19:21:03 -07:00
commit 7e467501a2
40 changed files with 2537 additions and 807 deletions

4
Godeps/Godeps.json generated
View File

@ -43,11 +43,11 @@
}, },
{ {
"ImportPath": "github.com/docker/rufus/proto", "ImportPath": "github.com/docker/rufus/proto",
"Rev": "61b53384b24bfa83e8e0a5f11f28ae83457fd80c" "Rev": "7f61f678c264ae0a329f25cbaa8af6fd55ada7b6"
}, },
{ {
"ImportPath": "github.com/endophage/gotuf", "ImportPath": "github.com/endophage/gotuf",
"Rev": "682ec56d6a7b60e432bc2560e17d8e1aec84d171" "Rev": "66da486b58ef378c96433af965f61ca0efaccb9a"
}, },
{ {
"ImportPath": "github.com/go-sql-driver/mysql", "ImportPath": "github.com/go-sql-driver/mysql",

View File

@ -10,6 +10,7 @@ It is generated from these files:
It has these top-level messages: It has these top-level messages:
KeyID KeyID
Algorithm
PublicKey PublicKey
Signature Signature
SignatureRequest SignatureRequest
@ -40,10 +41,20 @@ func (m *KeyID) Reset() { *m = KeyID{} }
func (m *KeyID) String() string { return proto1.CompactTextString(m) } func (m *KeyID) String() string { return proto1.CompactTextString(m) }
func (*KeyID) ProtoMessage() {} func (*KeyID) ProtoMessage() {}
// PublicKey has a KeyID that is used to reference the key and opaque bytes of a publicKey // Type holds the type of crypto algorithm used
type Algorithm struct {
Algorithm string `protobuf:"bytes,1,opt,name=algorithm" json:"algorithm,omitempty"`
}
func (m *Algorithm) Reset() { *m = Algorithm{} }
func (m *Algorithm) String() string { return proto1.CompactTextString(m) }
func (*Algorithm) ProtoMessage() {}
// PublicKey has a KeyID that is used to reference the key, the key type, and opaque bytes of a publicKey
type PublicKey struct { type PublicKey struct {
KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"`
PublicKey []byte `protobuf:"bytes,2,opt,name=publicKey,proto3" json:"publicKey,omitempty"` Algorithm *Algorithm `protobuf:"bytes,2,opt,name=algorithm" json:"algorithm,omitempty"`
PublicKey []byte `protobuf:"bytes,3,opt,name=publicKey,proto3" json:"publicKey,omitempty"`
} }
func (m *PublicKey) Reset() { *m = PublicKey{} } func (m *PublicKey) Reset() { *m = PublicKey{} }
@ -57,10 +68,18 @@ func (m *PublicKey) GetKeyID() *KeyID {
return nil return nil
} }
// Signature specifies a KeyID that was used for signing and signed content func (m *PublicKey) GetAlgorithm() *Algorithm {
if m != nil {
return m.Algorithm
}
return nil
}
// Signature specifies a KeyID that was used for signing, the key type, and signed content
type Signature struct { type Signature struct {
KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"`
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` Algorithm *Algorithm `protobuf:"bytes,2,opt,name=algorithm" json:"algorithm,omitempty"`
Content []byte `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
} }
func (m *Signature) Reset() { *m = Signature{} } func (m *Signature) Reset() { *m = Signature{} }
@ -74,10 +93,18 @@ func (m *Signature) GetKeyID() *KeyID {
return nil return nil
} }
// SignatureRequests specifies a KeyID for signing and content to be signed func (m *Signature) GetAlgorithm() *Algorithm {
if m != nil {
return m.Algorithm
}
return nil
}
// SignatureRequests specifies a KeyID for signing, the type of signature requested, and content to be signed
type SignatureRequest struct { type SignatureRequest struct {
KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"`
Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` Algorithm *Algorithm `protobuf:"bytes,2,opt,name=algorithm" json:"algorithm,omitempty"`
Content []byte `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
} }
func (m *SignatureRequest) Reset() { *m = SignatureRequest{} } func (m *SignatureRequest) Reset() { *m = SignatureRequest{} }
@ -91,6 +118,13 @@ func (m *SignatureRequest) GetKeyID() *KeyID {
return nil return nil
} }
func (m *SignatureRequest) GetAlgorithm() *Algorithm {
if m != nil {
return m.Algorithm
}
return nil
}
// Void represents an empty message type // Void represents an empty message type
type Void struct { type Void struct {
} }

View File

@ -26,22 +26,30 @@ message KeyID {
string ID = 1; string ID = 1;
} }
// PublicKey has a KeyID that is used to reference the key and opaque bytes of a publicKey // Type holds the type of crypto algorithm used
message Algorithm {
string algorithm = 1;
}
// PublicKey has a KeyID that is used to reference the key, the key type, and opaque bytes of a publicKey
message PublicKey { message PublicKey {
KeyID keyID = 1; KeyID keyID = 1;
bytes publicKey = 2; Algorithm algorithm = 2;
bytes publicKey = 3;
} }
// Signature specifies a KeyID that was used for signing and signed content // Signature specifies a KeyID that was used for signing, the key type, and signed content
message Signature { message Signature {
KeyID keyID = 1; KeyID keyID = 1;
bytes content = 2; Algorithm algorithm = 2;
bytes content = 3;
} }
// SignatureRequests specifies a KeyID for signing and content to be signed // SignatureRequests specifies a KeyID for signing, the type of signature requested, and content to be signed
message SignatureRequest { message SignatureRequest {
KeyID keyID = 1; KeyID keyID = 1;
bytes content = 2; Algorithm algorithm = 2;
bytes content = 3;
} }
// Void represents an empty message type // Void represents an empty message type

View File

@ -0,0 +1,3 @@
Aaron Lehmann <aaron.lehmann@docker.com> (github: aaronlehmann)
Lewis Marshall <lewis@flynn.io> (github: lmars)
Jonathan Rudenberg <jonathan@flynn.io> (github: titanous)

View File

@ -1,14 +1,22 @@
package signed package signed
import ( import (
"encoding/pem"
"testing" "testing"
"github.com/endophage/gotuf/data" "github.com/endophage/gotuf/data"
"github.com/endophage/gotuf/keys" )
const (
testKeyPEM1 = "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnKuXZeefa2LmgxaL5NsM\nzKOHNe+x/nL6ik+lDBCTV6OdcwAhHQS+PONGhrChIUVR6Vth3hUCrreLzPO73Oo5\nVSCuRJ53UronENl6lsa5mFKP8StYLvIDITNvkoT3j52BJIjyNUK9UKY9As2TNqDf\nBEPIRp28ev/NViwGOEkBu2UAbwCIdnDXm8JQErCZA0Ydm7PKGgjLbFsFGrVzqXHK\n6pdzJXlhr9yap3UpgQ/iO9JtoEYB2EXsnSrPc9JRjR30bNHHtnVql3fvinXrAEwq\n3xmN4p+R4VGzfdQN+8Kl/IPjqWB535twhFYEG/B7Ze8IwbygBjK3co/KnOPqMUrM\nBI8ztvPiogz+MvXb8WvarZ6TMTh8ifZI96r7zzqyzjR1hJulEy3IsMGvz8XS2J0X\n7sXoaqszEtXdq5ef5zKVxkiyIQZcbPgmpHLq4MgfdryuVVc/RPASoRIXG4lKaTJj\n1ANMFPxDQpHudCLxwCzjCb+sVa20HBRPTnzo8LSZkI6jAgMBAAE=\n-----END PUBLIC KEY-----"
testKeyID1 = "51324b59d4888faa91219ebbe5a3876bb4efb21f0602ddf363cd4c3996ded3d4"
testKeyPEM2 = "-----BEGIN PUBLIC KEY-----\nMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEArvqUPYb6JJROPJQglPTj\n5uDrsxQKl34Mo+3pSlBVuD6puE4lDnG649a2YksJy+C8ZIPJgokn5w+C3alh+dMe\nzbdWHHxrY1h9CLpYz5cbMlE16303ubkt1rvwDqEezG0HDBzPaKj4oP9YJ9x7wbsq\ndvFcy+Qc3wWd7UWcieo6E0ihbJkYcY8chRXVLg1rL7EfZ+e3bq5+ojA2ECM5JqzZ\nzgDpqCv5hTCYYZp72MZcG7dfSPAHrcSGIrwg7whzz2UsEtCOpsJTuCl96FPN7kAu\n4w/WyM3+SPzzr4/RQXuY1SrLCFD8ebM2zHt/3ATLhPnGmyG5I0RGYoegFaZ2AViw\nlqZDOYnBtgDvKP0zakMtFMbkh2XuNBUBO7Sjs0YcZMjLkh9gYUHL1yWS3Aqus1Lw\nlI0gHS22oyGObVBWkZEgk/Foy08sECLGao+5VvhmGpfVuiz9OKFUmtPVjWzRE4ng\niekEu4drSxpH41inLGSvdByDWLpcTvWQI9nkgclh3AT/AgMBAAE=\n-----END PUBLIC KEY-----"
testKeyID2 = "26f2f5c0fbfa98823bf1ad39d5f3b32575895793baf80f1df675597d5b95dba8"
) )
type MockCryptoService struct { type MockCryptoService struct {
testKey keys.PublicKey testKey data.PublicKey
} }
func (mts *MockCryptoService) Sign(keyIDs []string, _ []byte) ([]data.Signature, error) { func (mts *MockCryptoService) Sign(keyIDs []string, _ []byte) ([]data.Signature, error) {
@ -19,12 +27,12 @@ func (mts *MockCryptoService) Sign(keyIDs []string, _ []byte) ([]data.Signature,
return sigs, nil return sigs, nil
} }
func (mts *MockCryptoService) Create() (*keys.PublicKey, error) { func (mts *MockCryptoService) Create(_ string) (*data.PublicKey, error) {
return &mts.testKey, nil return &mts.testKey, nil
} }
func (mts *MockCryptoService) PublicKeys(keyIDs ...string) (map[string]*keys.PublicKey, error) { func (mts *MockCryptoService) PublicKeys(keyIDs ...string) (map[string]*data.PublicKey, error) {
keys := map[string]*keys.PublicKey{"testID": &mts.testKey} keys := map[string]*data.PublicKey{"testID": &mts.testKey}
return keys, nil return keys, nil
} }
@ -32,8 +40,10 @@ var _ CryptoService = &MockCryptoService{}
// Test signing and ensure the expected signature is added // Test signing and ensure the expected signature is added
func TestBasicSign(t *testing.T) { func TestBasicSign(t *testing.T) {
testKey, _ := pem.Decode([]byte(testKeyPEM1))
k := data.NewPublicKey("rsa", testKey.Bytes)
signer := Signer{&MockCryptoService{ signer := Signer{&MockCryptoService{
testKey: keys.PublicKey{ID: "testID"}, testKey: *k,
}} }}
key, err := signer.Create("root") key, err := signer.Create("root")
if err != nil { if err != nil {
@ -47,7 +57,7 @@ func TestBasicSign(t *testing.T) {
t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures)) t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures))
} }
if testData.Signatures[0].KeyID != "testID" { if testData.Signatures[0].KeyID != testKeyID1 {
t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID) t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID)
} }
@ -57,20 +67,21 @@ func TestBasicSign(t *testing.T) {
// for the key (N.B. MockCryptoService.Sign will still be called again, but Signer.Sign // for the key (N.B. MockCryptoService.Sign will still be called again, but Signer.Sign
// should be cleaning previous signatures by the KeyID when asked to sign again) // should be cleaning previous signatures by the KeyID when asked to sign again)
func TestReSign(t *testing.T) { func TestReSign(t *testing.T) {
testKey, _ := pem.Decode([]byte(testKeyPEM1))
k := data.NewPublicKey("rsa", testKey.Bytes)
signer := Signer{&MockCryptoService{ signer := Signer{&MockCryptoService{
testKey: keys.PublicKey{}, testKey: *k,
}} }}
key := keys.PublicKey{ID: "testID"}
testData := data.Signed{} testData := data.Signed{}
signer.Sign(&testData, &key) signer.Sign(&testData, k)
signer.Sign(&testData, &key) signer.Sign(&testData, k)
if len(testData.Signatures) != 1 { if len(testData.Signatures) != 1 {
t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures)) t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures))
} }
if testData.Signatures[0].KeyID != "testID" { if testData.Signatures[0].KeyID != testKeyID1 {
t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID) t.Fatalf("Wrong signature ID returned: %s", testData.Signatures[0].KeyID)
} }
@ -78,19 +89,21 @@ func TestReSign(t *testing.T) {
func TestMultiSign(t *testing.T) { func TestMultiSign(t *testing.T) {
signer := Signer{&MockCryptoService{}} signer := Signer{&MockCryptoService{}}
key := keys.PublicKey{ID: "testID1"}
testData := data.Signed{} testData := data.Signed{}
signer.Sign(&testData, &key) testKey, _ := pem.Decode([]byte(testKeyPEM1))
key := data.NewPublicKey("rsa", testKey.Bytes)
signer.Sign(&testData, key)
key = keys.PublicKey{ID: "testID2"} testKey, _ = pem.Decode([]byte(testKeyPEM2))
signer.Sign(&testData, &key) key = data.NewPublicKey("rsa", testKey.Bytes)
signer.Sign(&testData, key)
if len(testData.Signatures) != 2 { if len(testData.Signatures) != 2 {
t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures)) t.Fatalf("Incorrect number of signatures: %d", len(testData.Signatures))
} }
keyIDs := map[string]struct{}{"testID1": struct{}{}, "testID2": struct{}{}} keyIDs := map[string]struct{}{testKeyID1: struct{}{}, testKeyID2: struct{}{}}
for _, sig := range testData.Signatures { for _, sig := range testData.Signatures {
if _, ok := keyIDs[sig.KeyID]; !ok { if _, ok := keyIDs[sig.KeyID]; !ok {
t.Fatalf("Got a signature we didn't expect: %s", sig.KeyID) t.Fatalf("Got a signature we didn't expect: %s", sig.KeyID)
@ -100,8 +113,10 @@ func TestMultiSign(t *testing.T) {
} }
func TestCreate(t *testing.T) { func TestCreate(t *testing.T) {
testKey, _ := pem.Decode([]byte(testKeyPEM1))
k := data.NewPublicKey("rsa", testKey.Bytes)
signer := Signer{&MockCryptoService{ signer := Signer{&MockCryptoService{
testKey: keys.PublicKey{ID: "testID"}, testKey: *k,
}} }}
key, err := signer.Create("root") key, err := signer.Create("root")
@ -109,7 +124,7 @@ func TestCreate(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if key.ID != "testID" { if key.ID() != testKeyID1 {
t.Fatalf("Expected key ID not found: %s", key.ID) t.Fatalf("Expected key ID not found: %s", key.ID())
} }
} }

View File

@ -1,10 +1,10 @@
package signed package signed
import ( import (
"crypto" _ "crypto"
"crypto/rsa" _ "crypto/rsa"
"crypto/sha256" _ "crypto/sha256"
"crypto/x509" _ "crypto/x509"
"testing" "testing"
) )

View File

@ -23,7 +23,7 @@ func (VerifySuite) Test(c *C) {
signer := NewSigner(trust) signer := NewSigner(trust)
type test struct { type test struct {
name string name string
keys []*keys.PublicKey keys []*data.PublicKey
roles map[string]*data.Role roles map[string]*data.Role
s *data.Signed s *data.Signed
ver int ver int
@ -81,7 +81,7 @@ func (VerifySuite) Test(c *C) {
k, _ := signer.Create("root") k, _ := signer.Create("root")
signer.Sign(t.s, k) signer.Sign(t.s, k)
t.keys = append(t.keys, k) t.keys = append(t.keys, k)
t.roles["root"].KeyIDs = append(t.roles["root"].KeyIDs, k.ID) t.roles["root"].KeyIDs = append(t.roles["root"].KeyIDs, k.ID())
}, },
}, },
{ {
@ -139,7 +139,7 @@ func (VerifySuite) Test(c *C) {
{ {
name: "expired", name: "expired",
exp: &expiredTime, exp: &expiredTime,
err: ErrExpired{expiredTime}, err: ErrExpired{expiredTime.Format("2006-01-02 15:04:05 MST")},
}, },
} }
for _, t := range tests { for _, t := range tests {
@ -154,24 +154,27 @@ func (VerifySuite) Test(c *C) {
t.exp = &expires t.exp = &expires
} }
if t.typ == "" { if t.typ == "" {
t.typ = t.role t.typ = data.TUFTypes[t.role]
} }
if t.keys == nil && t.s == nil { if t.keys == nil && t.s == nil {
k, _ := signer.Create("root") k, _ := signer.Create("root")
meta := &signedMeta{Type: t.typ, Version: t.ver, Expires: *t.exp} meta := &signedMeta{Type: t.typ, Version: t.ver, Expires: t.exp.Format("2006-01-02 15:04:05 MST")}
b, err := cjson.Marshal(meta) b, err := cjson.Marshal(meta)
c.Assert(err, IsNil) c.Assert(err, IsNil)
s := &data.Signed{Signed: b} s := &data.Signed{Signed: b}
signer.Sign(s, k) signer.Sign(s, k)
t.s = s t.s = s
t.keys = []*keys.PublicKey{k} t.keys = []*data.PublicKey{k}
} }
if t.roles == nil { if t.roles == nil {
t.roles = map[string]*data.Role{ t.roles = map[string]*data.Role{
"root": &data.Role{ "root": &data.Role{
KeyIDs: []string{t.keys[0].ID}, RootRole: data.RootRole{
Threshold: 1, KeyIDs: []string{t.keys[0].ID()},
Threshold: 1,
},
Name: "root",
}, },
} }
} }
@ -181,11 +184,10 @@ func (VerifySuite) Test(c *C) {
db := keys.NewDB() db := keys.NewDB()
for _, k := range t.keys { for _, k := range t.keys {
err := db.AddKey(k) db.AddKey(k)
c.Assert(err, IsNil)
} }
for n, r := range t.roles { for _, r := range t.roles {
err := db.AddRole(n, r) err := db.AddRole(r)
c.Assert(err, IsNil) c.Assert(err, IsNil)
} }
@ -203,5 +205,5 @@ func assertErrExpired(c *C, err error, expected ErrExpired) {
if !ok { if !ok {
c.Fatalf("expected err to have type ErrExpired, got %T", err) c.Fatalf("expected err to have type ErrExpired, got %T", err)
} }
c.Assert(actual.Expired.Unix(), Equals, expected.Expired.Unix()) c.Assert(actual.Expired, Equals, expected.Expired)
} }

View File

@ -0,0 +1,13 @@
package store
import (
"fmt"
)
type ErrMetaNotFound struct {
role string
}
func (err ErrMetaNotFound) Error() string {
return fmt.Sprintf("no metadata for %s", err.role)
}

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/http"
"net/url" "net/url"
"path" "path"
@ -61,6 +62,9 @@ func (s HTTPStore) GetMeta(name string, size int64) (json.RawMessage, error) {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, &ErrMetaNotFound{role: name}
}
b := io.LimitReader(resp.Body, int64(size)) b := io.LimitReader(resp.Body, int64(size))
body, err := ioutil.ReadAll(b) body, err := ioutil.ReadAll(b)

View File

@ -177,6 +177,22 @@ func (tr *TufRepo) UpdateDelegations(role *data.Role, keys []data.Key, before st
// also relies on the keysDB having already been populated with the keys and // also relies on the keysDB having already been populated with the keys and
// roles. // roles.
func (tr *TufRepo) InitRepo(consistent bool) error { func (tr *TufRepo) InitRepo(consistent bool) error {
err := tr.InitRoot(consistent)
if err != nil {
return err
}
tr.InitTargets()
if err != nil {
return err
}
tr.InitSnapshot()
if err != nil {
return err
}
return tr.InitTimestamp()
}
func (tr *TufRepo) InitRoot(consistent bool) error {
rootRoles := make(map[string]*data.RootRole) rootRoles := make(map[string]*data.RootRole)
rootKeys := make(map[string]*data.PublicKey) rootKeys := make(map[string]*data.PublicKey)
for _, r := range data.ValidRoles { for _, r := range data.ValidRoles {
@ -199,15 +215,21 @@ func (tr *TufRepo) InitRepo(consistent bool) error {
return err return err
} }
tr.Root = root tr.Root = root
return nil
}
func (tr *TufRepo) InitTargets() error {
targets := data.NewTargets() targets := data.NewTargets()
tr.Targets[data.ValidRoles["targets"]] = targets tr.Targets[data.ValidRoles["targets"]] = targets
return nil
}
signedRoot, err := tr.SignRoot(data.DefaultExpires("root")) func (tr *TufRepo) InitSnapshot() error {
signedRoot, err := tr.SignRoot(data.DefaultExpires("root"), nil)
if err != nil { if err != nil {
return err return err
} }
signedTargets, err := tr.SignTargets("targets", data.DefaultExpires("targets")) signedTargets, err := tr.SignTargets("targets", data.DefaultExpires("targets"), nil)
if err != nil { if err != nil {
return err return err
} }
@ -216,8 +238,11 @@ func (tr *TufRepo) InitRepo(consistent bool) error {
return err return err
} }
tr.Snapshot = snapshot tr.Snapshot = snapshot
return nil
}
signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot")) func (tr *TufRepo) InitTimestamp() error {
signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot"), nil)
if err != nil { if err != nil {
return err return err
} }
@ -428,7 +453,7 @@ func (tr *TufRepo) UpdateTimestamp(s *data.Signed) error {
return nil return nil
} }
func (tr *TufRepo) SignRoot(expires time.Time) (*data.Signed, error) { func (tr *TufRepo) SignRoot(expires time.Time, signer *signed.Signer) (*data.Signed, error) {
logrus.Debug("SignRoot") logrus.Debug("SignRoot")
if tr.Root.Dirty { if tr.Root.Dirty {
tr.Root.Signed.Version++ tr.Root.Signed.Version++
@ -438,7 +463,7 @@ func (tr *TufRepo) SignRoot(expires time.Time) (*data.Signed, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
signed, err = tr.sign(signed, *root) signed, err = tr.sign(signed, *root, signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -446,7 +471,7 @@ func (tr *TufRepo) SignRoot(expires time.Time) (*data.Signed, error) {
return signed, nil return signed, nil
} }
func (tr *TufRepo) SignTargets(role string, expires time.Time) (*data.Signed, error) { func (tr *TufRepo) SignTargets(role string, expires time.Time, signer *signed.Signer) (*data.Signed, error) {
logrus.Debug("SignTargets") logrus.Debug("SignTargets")
logrus.Debug("Got targets data.Signed object") logrus.Debug("Got targets data.Signed object")
if tr.Targets[role].Dirty { if tr.Targets[role].Dirty {
@ -458,7 +483,7 @@ func (tr *TufRepo) SignTargets(role string, expires time.Time) (*data.Signed, er
} }
targets := tr.keysDB.GetRole(role) targets := tr.keysDB.GetRole(role)
logrus.Debug("About to sign ", role) logrus.Debug("About to sign ", role)
signed, err = tr.sign(signed, *targets) signed, err = tr.sign(signed, *targets, signer)
if err != nil { if err != nil {
logrus.Debug("errored signing ", role) logrus.Debug("errored signing ", role)
return nil, err return nil, err
@ -476,10 +501,10 @@ func (tr *TufRepo) SignTargets(role string, expires time.Time) (*data.Signed, er
} }
} }
func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) { func (tr *TufRepo) SignSnapshot(expires time.Time, signer *signed.Signer) (*data.Signed, error) {
logrus.Debug("SignSnapshot") logrus.Debug("SignSnapshot")
if tr.Root.Dirty { if tr.Root.Dirty {
signedRoot, err := tr.SignRoot(data.DefaultExpires("root")) signedRoot, err := tr.SignRoot(data.DefaultExpires("root"), signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -493,7 +518,7 @@ func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) {
if !targets.Dirty { if !targets.Dirty {
continue continue
} }
signedTargets, err := tr.SignTargets(role, data.DefaultExpires("targets")) signedTargets, err := tr.SignTargets(role, data.DefaultExpires("targets"), signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -510,7 +535,7 @@ func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) {
return nil, err return nil, err
} }
snapshot := tr.keysDB.GetRole(data.ValidRoles["snapshot"]) snapshot := tr.keysDB.GetRole(data.ValidRoles["snapshot"])
signed, err = tr.sign(signed, *snapshot) signed, err = tr.sign(signed, *snapshot, signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -525,10 +550,10 @@ func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) {
} }
} }
func (tr *TufRepo) SignTimestamp(expires time.Time) (*data.Signed, error) { func (tr *TufRepo) SignTimestamp(expires time.Time, signer *signed.Signer) (*data.Signed, error) {
logrus.Debug("SignTimestamp") logrus.Debug("SignTimestamp")
if tr.Snapshot.Dirty { if tr.Snapshot.Dirty {
signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot")) signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot"), signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -544,7 +569,7 @@ func (tr *TufRepo) SignTimestamp(expires time.Time) (*data.Signed, error) {
return nil, err return nil, err
} }
timestamp := tr.keysDB.GetRole(data.ValidRoles["timestamp"]) timestamp := tr.keysDB.GetRole(data.ValidRoles["timestamp"])
signed, err = tr.sign(signed, *timestamp) signed, err = tr.sign(signed, *timestamp, signer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -560,7 +585,7 @@ func (tr *TufRepo) SignTimestamp(expires time.Time) (*data.Signed, error) {
} }
} }
func (tr TufRepo) sign(signed *data.Signed, role data.Role) (*data.Signed, error) { func (tr TufRepo) sign(signed *data.Signed, role data.Role, signer *signed.Signer) (*data.Signed, error) {
ks := make([]*data.PublicKey, 0, len(role.KeyIDs)) ks := make([]*data.PublicKey, 0, len(role.KeyIDs))
for _, kid := range role.KeyIDs { for _, kid := range role.KeyIDs {
k := tr.keysDB.GetKey(kid) k := tr.keysDB.GetKey(kid)
@ -572,9 +597,16 @@ func (tr TufRepo) sign(signed *data.Signed, role data.Role) (*data.Signed, error
if len(ks) < 1 { if len(ks) < 1 {
return nil, keys.ErrInvalidKey return nil, keys.ErrInvalidKey
} }
err := tr.signer.Sign(signed, ks...) if signer != nil {
if err != nil { err := signer.Sign(signed, ks...)
return nil, err if err != nil {
return nil, err
}
} else {
err := tr.signer.Sign(signed, ks...)
if err != nil {
return nil, err
}
} }
return signed, nil return signed, nil
} }

View File

@ -88,7 +88,7 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) signedRoot, err := repo.SignRoot(data.DefaultExpires("root"), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -96,7 +96,7 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) {
ioutil.WriteFile(dir+"/root.json", rootJSON, 0755) ioutil.WriteFile(dir+"/root.json", rootJSON, 0755)
for r, _ := range repo.Targets { for r, _ := range repo.Targets {
signedTargets, err := repo.SignTargets(r, data.DefaultExpires("targets")) signedTargets, err := repo.SignTargets(r, data.DefaultExpires("targets"), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -107,14 +107,14 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) {
ioutil.WriteFile(p, targetsJSON, 0755) ioutil.WriteFile(p, targetsJSON, 0755)
} }
signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot"), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
snapshotJSON, _ := json.Marshal(signedSnapshot) snapshotJSON, _ := json.Marshal(signedSnapshot)
ioutil.WriteFile(dir+"/snapshot.json", snapshotJSON, 0755) ioutil.WriteFile(dir+"/snapshot.json", snapshotJSON, 0755)
signedTimestamp, err := repo.SignTimestamp(data.DefaultExpires("timestamp")) signedTimestamp, err := repo.SignTimestamp(data.DefaultExpires("timestamp"), nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -0,0 +1,47 @@
package changelist
// TufChange represents a change to a TUF repo
type TufChange struct {
// Abbreviated because Go doesn't permit a field and method of the same name
Actn int `json:"action"`
Role string `json:"role"`
ChangeType string `json:"type"`
ChangePath string `json:"path"`
Data []byte `json:"data"`
}
// NewTufChange initializes a tufChange object
func NewTufChange(action int, role, changeType, changePath string, content []byte) *TufChange {
return &TufChange{
Actn: action,
Role: role,
ChangeType: changeType,
ChangePath: changePath,
Data: content,
}
}
// Action return c.Actn
func (c TufChange) Action() int {
return c.Actn
}
// Scope returns c.Role
func (c TufChange) Scope() string {
return c.Role
}
// Type returns c.ChangeType
func (c TufChange) Type() string {
return c.ChangeType
}
// Path return c.ChangePath
func (c TufChange) Path() string {
return c.ChangePath
}
// Content returns c.Data
func (c TufChange) Content() []byte {
return c.Data
}

View File

@ -0,0 +1,112 @@
package changelist
import (
"bufio"
"encoding/json"
"fmt"
"os"
)
// AppendChangelist represents a list of TUF changes
type AppendChangelist struct {
path string
file *os.File
closed bool
}
// NewAppendChangelist is a convinience method that returns an append only TUF
// change list
func NewAppendChangelist(path string) (*AppendChangelist, error) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
if err != nil {
return nil, err
}
return &AppendChangelist{
path: path,
file: file,
}, nil
}
// List returns a list of Changes
func (cl *AppendChangelist) List() []Change {
cl.file.Seek(0, 0) // seek to start of file
var changes []Change
scnr := bufio.NewScanner(cl.file)
for scnr.Scan() {
line := scnr.Bytes()
c := &TufChange{}
err := json.Unmarshal(line, c)
if err != nil {
// TODO(david): How should we handle this?
fmt.Println(err.Error())
continue
}
changes = append(changes, c)
}
return changes
}
// Add adds a change to the append only changelist
func (cl *AppendChangelist) Add(c Change) error {
cl.file.Seek(0, 2) // seek to end of file
entry, err := json.Marshal(c)
if err != nil {
return err
}
n, err := cl.file.Write(entry)
if err != nil {
if n > 0 {
// trim partial write if necessary
size, _ := cl.file.Seek(-int64(n), 2)
cl.file.Truncate(size)
}
return err
}
cl.file.Write([]byte("\n"))
cl.file.Sync()
return nil
}
// Clear empties the changelist file. It does not currently
// support archiving
func (cl *AppendChangelist) Clear(archive string) error {
cl.file.Seek(0, 0) // seek to start
cl.file.Truncate(0) // truncate
cl.file.Sync()
return nil
}
// Close marks the change list as closed
func (cl *AppendChangelist) Close() error {
cl.file.Sync()
cl.closed = true
return cl.file.Close()
}
// memChangeList implements a simple in memory change list.
type memChangelist struct {
changes []Change
}
// List returns a list of Changes
func (cl memChangelist) List() []Change {
return cl.changes
}
// Add adds a change to the in-memory change list
func (cl *memChangelist) Add(c Change) error {
cl.changes = append(cl.changes, c)
return nil
}
// Clear empties the changelist file.
func (cl *memChangelist) Clear(archive string) error {
// appending to a nil list initializes it.
cl.changes = nil
return nil
}
// Close is a no-op in this in-memory change-list
func (cl *memChangelist) Close() error {
return nil
}

View File

@ -0,0 +1,41 @@
package changelist
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileChangelist(t *testing.T) {
tmpDir, err := ioutil.TempDir("/tmp", "test")
if err != nil {
t.Fatal(err.Error())
}
defer os.RemoveAll(tmpDir)
file := path.Join(tmpDir, "list")
cl, err := NewAppendChangelist(file)
assert.Nil(t, err, "Error initializing appendChangelist")
c := NewTufChange(ActionCreate, "targets", "target", "test/targ", []byte{1})
err = cl.Add(c)
assert.Nil(t, err, "Non-nil error while adding change")
cs := cl.List()
assert.Equal(t, 1, len(cs), "List should have returned exactly one item")
assert.Equal(t, c.Action(), cs[0].Action(), "Action mismatch")
assert.Equal(t, c.Scope(), cs[0].Scope(), "Scope mismatch")
assert.Equal(t, c.Type(), cs[0].Type(), "Type mismatch")
assert.Equal(t, c.Path(), cs[0].Path(), "Path mismatch")
assert.Equal(t, c.Content(), cs[0].Content(), "Content mismatch")
err = cl.Clear("")
assert.Nil(t, err, "Non-nil error while clearing")
cs = cl.List()
assert.Equal(t, 0, len(cs), "List should be empty")
}

View File

@ -0,0 +1,116 @@
package changelist
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"sort"
"time"
"code.google.com/p/go-uuid/uuid"
"github.com/Sirupsen/logrus"
)
// FileChangelist stores all the changes as files
type FileChangelist struct {
dir string
}
// NewFileChangelist is a convenience method for returning FileChangeLists
func NewFileChangelist(dir string) (*FileChangelist, error) {
logrus.Debug("Making dir path: ", dir)
err := os.MkdirAll(dir, 0700)
if err != nil {
return nil, err
}
return &FileChangelist{dir: dir}, nil
}
// List returns a list of sorted changes
func (cl FileChangelist) List() []Change {
var changes []Change
dir, err := os.Open(cl.dir)
if err != nil {
return changes
}
defer dir.Close()
fileInfos, err := dir.Readdir(0)
if err != nil {
return changes
}
sort.Sort(fileChanges(fileInfos))
for _, f := range fileInfos {
if f.IsDir() {
continue
}
raw, err := ioutil.ReadFile(path.Join(cl.dir, f.Name()))
if err != nil {
// TODO(david): How should we handle this?
fmt.Println(err.Error())
continue
}
c := &TufChange{}
err = json.Unmarshal(raw, c)
if err != nil {
// TODO(david): How should we handle this?
fmt.Println(err.Error())
continue
}
changes = append(changes, c)
}
return changes
}
// Add adds a change to the file change list
func (cl FileChangelist) Add(c Change) error {
cJSON, err := json.Marshal(c)
if err != nil {
return err
}
filename := fmt.Sprintf("%020d_%s.change", time.Now().UnixNano(), uuid.New())
return ioutil.WriteFile(path.Join(cl.dir, filename), cJSON, 0644)
}
// Clear clears the change list
func (cl FileChangelist) Clear(archive string) error {
dir, err := os.Open(cl.dir)
if err != nil {
return err
}
defer dir.Close()
files, err := dir.Readdir(0)
if err != nil {
return err
}
for _, f := range files {
os.Remove(path.Join(cl.dir, f.Name()))
}
return nil
}
// Close is a no-op
func (cl FileChangelist) Close() error {
// Nothing to do here
return nil
}
type fileChanges []os.FileInfo
// Len returns the length of a file change list
func (cs fileChanges) Len() int {
return len(cs)
}
// Less compares the names of two different file changes
func (cs fileChanges) Less(i, j int) bool {
return cs[i].Name() < cs[j].Name()
}
// Swap swaps the position of two file changes
func (cs fileChanges) Swap(i, j int) {
tmp := cs[i]
cs[i] = cs[j]
cs[j] = tmp
}

View File

@ -0,0 +1,76 @@
package changelist
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
tmpDir, err := ioutil.TempDir("/tmp", "test")
if err != nil {
t.Fatal(err.Error())
}
defer os.RemoveAll(tmpDir)
cl, err := NewFileChangelist(tmpDir)
assert.Nil(t, err, "Error initializing fileChangelist")
c := NewTufChange(ActionCreate, "targets", "target", "test/targ", []byte{1})
err = cl.Add(c)
assert.Nil(t, err, "Non-nil error while adding change")
cs := cl.List()
assert.Equal(t, 1, len(cs), "List should have returned exactly one item")
assert.Equal(t, c.Action(), cs[0].Action(), "Action mismatch")
assert.Equal(t, c.Scope(), cs[0].Scope(), "Scope mismatch")
assert.Equal(t, c.Type(), cs[0].Type(), "Type mismatch")
assert.Equal(t, c.Path(), cs[0].Path(), "Path mismatch")
assert.Equal(t, c.Content(), cs[0].Content(), "Content mismatch")
err = cl.Clear("")
assert.Nil(t, err, "Non-nil error while clearing")
cs = cl.List()
assert.Equal(t, 0, len(cs), "List should be empty")
err = os.Remove(tmpDir) // will error if anything left in dir
assert.Nil(t, err, "Clear should have left the tmpDir empty")
}
func TestListOrder(t *testing.T) {
tmpDir, err := ioutil.TempDir("/tmp", "test")
if err != nil {
t.Fatal(err.Error())
}
defer os.RemoveAll(tmpDir)
cl, err := NewFileChangelist(tmpDir)
assert.Nil(t, err, "Error initializing fileChangelist")
c1 := NewTufChange(ActionCreate, "targets", "target", "test/targ1", []byte{1})
err = cl.Add(c1)
assert.Nil(t, err, "Non-nil error while adding change")
c2 := NewTufChange(ActionCreate, "targets", "target", "test/targ2", []byte{1})
err = cl.Add(c2)
assert.Nil(t, err, "Non-nil error while adding change")
cs := cl.List()
assert.Equal(t, 2, len(cs), "List should have returned exactly one item")
assert.Equal(t, c1.Action(), cs[0].Action(), "Action mismatch")
assert.Equal(t, c1.Scope(), cs[0].Scope(), "Scope mismatch")
assert.Equal(t, c1.Type(), cs[0].Type(), "Type mismatch")
assert.Equal(t, c1.Path(), cs[0].Path(), "Path mismatch")
assert.Equal(t, c1.Content(), cs[0].Content(), "Content mismatch")
assert.Equal(t, c2.Action(), cs[1].Action(), "Action 2 mismatch")
assert.Equal(t, c2.Scope(), cs[1].Scope(), "Scope 2 mismatch")
assert.Equal(t, c2.Type(), cs[1].Type(), "Type 2 mismatch")
assert.Equal(t, c2.Path(), cs[1].Path(), "Path 2 mismatch")
assert.Equal(t, c2.Content(), cs[1].Content(), "Content 2 mismatch")
}

View File

@ -0,0 +1,59 @@
package changelist
// Changelist is the interface for all TUF change lists
type Changelist interface {
// List returns the ordered list of changes
// currently stored
List() []Change
// Add change appends the provided change to
// the list of changes
Add(Change) error
// Clear empties the current change list.
// Archive may be provided as a directory path
// to save a copy of the changelist in that location
Clear(archive string) error
// Close syncronizes any pending writes to the underlying
// storage and closes the file/connection
Close() error
}
const (
// ActionCreate represents a Create action
ActionCreate = iota
// ActionUpdate represents an Update action
ActionUpdate
// ActionDelete represents a Delete action
ActionDelete
)
// Change is the interface for a TUF Change
type Change interface {
// "create","update", or "delete"
Action() int
// Where the change should be made.
// For TUF this will be the role
Scope() string
// The content type being affected.
// For TUF this will be "target", or "delegation".
// If the type is "delegation", the Scope will be
// used to determine if a root role is being updated
// or a target delegation.
Type() string
// Path indicates the entry within a role to be affected by the
// change. For targets, this is simply the target's path,
// for delegations it's the delegated role name.
Path() string
// Serialized content that the interpreter of a changelist
// can use to apply the change.
// For TUF this will be the serialized JSON that needs
// to be inserted or merged. In the case of a "delete"
// action, it will be nil.
Content() []byte
}

View File

@ -0,0 +1,143 @@
package client
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"path/filepath"
"github.com/docker/notary/trustmanager"
"github.com/endophage/gotuf/data"
)
// CryptoService implements Sign and Create, holding a specific GUN and keystore to
// operate on
type CryptoService struct {
gun string
keyStore *trustmanager.KeyFileStore
}
// RootCryptoService implements Sign and Create and operates on a rootKeyStore,
// taking in a passphrase and calling decrypt when signing.
type RootCryptoService struct {
// TODO(diogo): support multiple passphrases per key
passphrase string
rootKeyStore *trustmanager.KeyFileStore
}
// NewCryptoService returns an instance of CryptoService
func NewCryptoService(gun string, keyStore *trustmanager.KeyFileStore) *CryptoService {
return &CryptoService{gun: gun, keyStore: keyStore}
}
// NewRootCryptoService returns an instance of CryptoService
func NewRootCryptoService(rootKeyStore *trustmanager.KeyFileStore, passphrase string) *RootCryptoService {
return &RootCryptoService{rootKeyStore: rootKeyStore, passphrase: passphrase}
}
// Create is used to generate keys for targets, snapshots and timestamps
func (ccs *CryptoService) Create(role string) (*data.PublicKey, error) {
privKey, err := trustmanager.GenerateRSAKey(rand.Reader, rsaKeySize)
if err != nil {
return nil, fmt.Errorf("failed to generate RSA key: %v", err)
}
// Store the private key into our keystore with the name being: /GUN/ID.key
ccs.keyStore.AddKey(filepath.Join(ccs.gun, privKey.ID()), privKey)
return data.PublicKeyFromPrivate(*privKey), nil
}
// Sign returns the signatures for data with the given keyIDs
func (ccs *CryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) {
// Create hasher and hash data
hash := crypto.SHA256
hashed := sha256.Sum256(payload)
signatures := make([]data.Signature, 0, len(keyIDs))
for _, fingerprint := range keyIDs {
// Get the PrivateKey filename
privKeyFilename := filepath.Join(ccs.gun, fingerprint)
// Read PrivateKey from file
privKey, err := ccs.keyStore.GetKey(privKeyFilename)
if err != nil {
continue
}
sig, err := sign(privKey, hash, hashed[:])
if err != nil {
return nil, err
}
// Append signatures to result array
signatures = append(signatures, data.Signature{
KeyID: fingerprint,
Method: "RSA",
Signature: sig[:],
})
}
return signatures, nil
}
// Create in a root crypto service is not implemented
func (rcs *RootCryptoService) Create(role string) (*data.PublicKey, error) {
return nil, errors.New("create on a root key filestore is not implemented")
}
// Sign returns the signatures for data with the given root Key ID, falling back
// if not rootKeyID is found
// TODO(diogo): This code has 1 line change from the Sign from Crypto service. DRY it up.
func (rcs *RootCryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) {
// Create hasher and hash data
hash := crypto.SHA256
hashed := sha256.Sum256(payload)
signatures := make([]data.Signature, 0, len(keyIDs))
for _, fingerprint := range keyIDs {
// Read PrivateKey from file
privKey, err := rcs.rootKeyStore.GetDecryptedKey(fingerprint, rcs.passphrase)
if err != nil {
// TODO(diogo): This error should be returned to the user in someway
continue
}
sig, err := sign(privKey, hash, hashed[:])
if err != nil {
return nil, err
}
// Append signatures to result array
signatures = append(signatures, data.Signature{
KeyID: fingerprint,
Method: "RSASSA-PKCS1-V1_5-SIGN",
Signature: sig[:],
})
}
return signatures, nil
}
func sign(privKey *data.PrivateKey, hash crypto.Hash, hashed []byte) ([]byte, error) {
// TODO(diogo): Implement support for ECDSA.
if privKey.Cipher() != "RSA" {
return nil, fmt.Errorf("private key type not supported: %s", privKey.Cipher())
}
// Create an rsa.PrivateKey out of the private key bytes
rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKey.Private())
if err != nil {
return nil, err
}
// Use the RSA key to sign the data
sig, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivKey, hash, hashed[:])
if err != nil {
return nil, err
}
return sig, nil
}

717
client/client.go Normal file
View File

@ -0,0 +1,717 @@
package client
import (
"bytes"
"crypto/rand"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/notary/client/changelist"
"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"
)
// ErrRepoNotInitialized is returned when trying to can publish on an uninitialized
// notary repository
type ErrRepoNotInitialized struct{}
type passwordRetriever func() (string, error)
// ErrRepoNotInitialized is returned when trying to can publish on an uninitialized
// notary repository
func (err *ErrRepoNotInitialized) Error() string {
return "Repository has not been initialized"
}
// Default paths should end with a '/' so directory creation works correctly
const (
trustDir string = "/trusted_certificates/"
privDir string = "/private/"
tufDir string = "/tuf/"
rootKeysDir string = privDir + "/root_keys/"
rsaKeySize int = 2048 // Used for snapshots and targets keys
rsaRootKeySize int = 4096 // Used for new 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")
// UnlockedSigner encapsulates a private key and a signer that uses that private key,
// providing convinience methods for generation of certificates.
type UnlockedSigner struct {
privKey *data.PrivateKey
signer *signed.Signer
}
// NotaryRepository stores all the information needed to operate on a notary
// repository.
type NotaryRepository struct {
baseDir string
Gun string
baseURL string
tufRepoPath string
caStore trustmanager.X509Store
certificateStore trustmanager.X509Store
fileStore store.MetadataStore
signer *signed.Signer
tufRepo *tuf.TufRepo
privKeyStore *trustmanager.KeyFileStore
rootKeyStore *trustmanager.KeyFileStore
rootSigner *UnlockedSigner
}
// Target represents a simplified version of the data TUF operates on, so external
// applications don't have to depend on tuf data types.
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
}
// NewNotaryRepository is a helper method that returns a new notary repository.
// It takes the base directory under where all the trust files will be stored
// (usually ~/.docker/trust/).
func NewNotaryRepository(baseDir, gun, baseURL string) (*NotaryRepository, error) {
trustDir := filepath.Join(baseDir, trustDir)
rootKeysDir := filepath.Join(baseDir, rootKeysDir)
privKeyStore, err := trustmanager.NewKeyFileStore(filepath.Join(baseDir, privDir))
if err != nil {
return nil, err
}
signer := signed.NewSigner(NewCryptoService(gun, privKeyStore))
nRepo := &NotaryRepository{
Gun: gun,
baseDir: baseDir,
baseURL: baseURL,
tufRepoPath: filepath.Join(baseDir, tufDir, gun),
signer: signer,
privKeyStore: privKeyStore,
}
if err := nRepo.loadKeys(trustDir, rootKeysDir); err != nil {
return nil, err
}
return nRepo, nil
}
// Initialize creates a new repository by using rootKey as the root Key for the
// TUF repository.
func (r *NotaryRepository) Initialize(uSigner *UnlockedSigner) error {
rootCert, err := uSigner.GenerateCertificate(r.Gun)
if err != nil {
return err
}
r.certificateStore.AddCert(rootCert)
rootKey := data.NewPublicKey("RSA", trustmanager.CertToPEM(rootCert))
err = r.rootKeyStore.Link(uSigner.ID(), rootKey.ID())
if err != nil {
return err
}
remote, err := getRemoteStore(r.baseURL, r.Gun)
rawTSKey, err := remote.GetKey("timestamp")
if err != nil {
return err
}
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
}
if err := kdb.AddRole(rootRole); err != nil {
return err
}
if err := kdb.AddRole(targetsRole); err != nil {
return err
}
if err := kdb.AddRole(snapshotRole); err != nil {
return err
}
if err := kdb.AddRole(timestampRole); err != nil {
return err
}
r.tufRepo = tuf.NewTufRepo(kdb, r.signer)
r.fileStore, err = store.NewFilesystemStore(
r.tufRepoPath,
"metadata",
"json",
"targets",
)
if err != nil {
return err
}
if err := r.tufRepo.InitRepo(false); err != nil {
return err
}
if err := r.saveMetadata(uSigner.signer); err != nil {
return err
}
// Creates an empty snapshot
return r.snapshot()
}
// AddTarget adds a new target to the repository, forcing a timestamps check from TUF
func (r *NotaryRepository) AddTarget(target *Target) error {
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
return err
}
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}
metaJSON, err := json.Marshal(meta)
if err != nil {
return err
}
c := changelist.NewTufChange(changelist.ActionCreate, "targets", "target", target.Name, metaJSON)
err = cl.Add(c)
if err != nil {
return err
}
return cl.Close()
}
// 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
}
var targetList []*Target
for name, meta := range r.tufRepo.Targets["targets"].Signed.Targets {
target := &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}
targetList = append(targetList, target)
}
return targetList, 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
// Conceptually it performs an operation similar to a `git rebase`
func (r *NotaryRepository) Publish(getPass passwordRetriever) error {
var updateRoot bool
var root *data.Signed
// attempt to initialize the repo from the remote store
c, err := r.bootstrapClient()
if err != nil {
if _, ok := err.(*store.ErrMetaNotFound); ok {
// if the remote store return a 404 (translated into ErrMetaNotFound),
// the repo hasn't been initialized yet. Attempt to load it from disk.
err := r.bootstrapRepo()
if err != nil {
// Repo hasn't been initialized, It must be initialized before
// it can be published. Return an error and let caller determine
// what it wants to do.
logrus.Debug("Repository not initialized during Publish")
return &ErrRepoNotInitialized{}
}
// We had local data but the server doesn't know about the repo yet,
// ensure we will push the initial root file
root, err = r.tufRepo.Root.ToSigned()
if err != nil {
return err
}
updateRoot = true
} else {
// The remote store returned an error other than 404. We're
// unable to determine if the repo has been initialized or not.
logrus.Error("Could not publish Repository: ", err.Error())
return err
}
} else {
// If we were successfully able to bootstrap the client (which only pulls
// root.json), update it the rest of the tuf metadata in preparation for
// applying the changelist.
err = c.Update()
if err != nil {
return err
}
}
// load the changelist for this repo
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
if err != nil {
logrus.Debug("Error initializing changelist")
return err
}
// apply the changelist to the repo
err = applyChangelist(r.tufRepo, cl)
if err != nil {
logrus.Debug("Error applying changelist")
return err
}
// check if our root file is nearing expiry. Resign if it is.
if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty {
passphrase, err := getPass()
if err != nil {
return err
}
rootKeyID := r.tufRepo.Root.Signed.Roles["root"].KeyIDs[0]
rootSigner, err := r.GetRootSigner(rootKeyID, passphrase)
if err != nil {
return err
}
root, err = r.tufRepo.SignRoot(data.DefaultExpires("root"), rootSigner.signer)
if err != nil {
return err
}
updateRoot = true
}
// we will always resign targets and snapshots
targets, err := r.tufRepo.SignTargets("targets", data.DefaultExpires("targets"), nil)
if err != nil {
return err
}
snapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"), nil)
if err != nil {
return err
}
remote, err := getRemoteStore(r.baseURL, r.Gun)
if err != nil {
return err
}
// ensure we can marshal all the json before sending anything to remote
targetsJSON, err := json.Marshal(targets)
if err != nil {
return err
}
snapshotJSON, err := json.Marshal(snapshot)
if err != nil {
return err
}
// if we need to update the root, marshal it and push the update to remote
if updateRoot {
rootJSON, err := json.Marshal(root)
if err != nil {
return err
}
err = remote.SetMeta("root", rootJSON)
if err != nil {
return err
}
}
err = remote.SetMeta("targets", targetsJSON)
if err != nil {
return err
}
err = remote.SetMeta("snapshot", snapshotJSON)
if err != nil {
return err
}
return nil
}
func (r *NotaryRepository) bootstrapRepo() error {
fileStore, err := store.NewFilesystemStore(
r.tufRepoPath,
"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) saveMetadata(rootSigner *signed.Signer) error {
signedRoot, err := r.tufRepo.SignRoot(data.DefaultExpires("root"), rootSigner)
if err != nil {
return err
}
rootJSON, _ := json.Marshal(signedRoot)
return r.fileStore.SetMeta("root", rootJSON)
}
func (r *NotaryRepository) snapshot() error {
fmt.Println("Saving changes to Trusted Collection.")
for t := range r.tufRepo.Targets {
signedTargets, err := r.tufRepo.SignTargets(t, data.DefaultExpires("targets"), nil)
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"), nil)
if err != nil {
return err
}
snapshotJSON, _ := json.Marshal(signedSnapshot)
return r.fileStore.SetMeta("snapshot", snapshotJSON)
}
/*
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[fingerprint].Public()))
logrus.Debug("Root PEM: ", k)
logrus.Debug("Root ID: ", fingerprint)
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]
}
}
if len(certs) < 1 {
return errors.New("could not validate the path to a trusted root")
}
_, err = signed.VerifyRoot(root, 0, certs, 1)
return err
}
func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
remote, err := getRemoteStore(r.baseURL, r.Gun)
if err != nil {
return nil, err
}
rootJSON, err := remote.GetMeta("root", 5<<20)
if err != nil {
return nil, err
}
root := &data.Signed{}
err = json.Unmarshal(rootJSON, root)
if err != nil {
return nil, err
}
err = r.validateRoot(root)
if err != nil {
return nil, err
}
kdb := keys.NewDB()
r.tufRepo = tuf.NewTufRepo(kdb, r.signer)
err = r.tufRepo.SetRoot(root)
if err != nil {
return nil, err
}
return tufclient.NewClient(
r.tufRepo,
remote,
kdb,
), nil
}
// ListRootKeys returns the IDs for all of the root keys. It ignores symlinks
// if any exist.
func (r *NotaryRepository) ListRootKeys() []string {
return r.rootKeyStore.ListKeys()
}
// GenRootKey generates a new root key protected by a given passphrase
func (r *NotaryRepository) GenRootKey(passphrase string) (string, error) {
privKey, err := trustmanager.GenerateRSAKey(rand.Reader, rsaRootKeySize)
if err != nil {
return "", fmt.Errorf("failed to convert private key: %v", err)
}
r.rootKeyStore.AddEncryptedKey(privKey.ID(), privKey, passphrase)
return privKey.ID(), nil
}
// GetRootSigner retreives a root key that includes the ID and a signer
func (r *NotaryRepository) GetRootSigner(rootKeyID, passphrase string) (*UnlockedSigner, error) {
privKey, err := r.rootKeyStore.GetDecryptedKey(rootKeyID, passphrase)
if err != nil {
return nil, fmt.Errorf("could not get decrypted root key: %v", err)
}
// This signer will be used for all of the normal TUF operations, except for
// when a root key is needed.
signer := signed.NewSigner(NewRootCryptoService(r.rootKeyStore, passphrase))
return &UnlockedSigner{
privKey: privKey,
signer: signer}, nil
}
func (r *NotaryRepository) loadKeys(trustDir, rootKeysDir string) error {
// Load all CAs that aren't expired and don't use SHA1
caStore, err := trustmanager.NewX509FilteredFileStore(trustDir, 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
certificateStore, err := trustmanager.NewX509FilteredFileStore(trustDir, 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
}
// Load the keystore that will hold all of our encrypted Root Private Keys
rootKeyStore, err := trustmanager.NewKeyFileStore(rootKeysDir)
if err != nil {
return err
}
r.caStore = caStore
r.certificateStore = certificateStore
r.rootKeyStore = rootKeyStore
return nil
}
// ID gets a consistent ID based on the PrivateKey bytes and cipher type
func (uk *UnlockedSigner) ID() string {
return uk.PublicKey().ID()
}
// PublicKey Returns the public key associated with the Private Key within the Signer
func (uk *UnlockedSigner) PublicKey() *data.PublicKey {
return data.PublicKeyFromPrivate(*uk.privKey)
}
// GenerateCertificate generates an X509 Certificate from a template, given a GUN
func (uk *UnlockedSigner) GenerateCertificate(gun string) (*x509.Certificate, error) {
privKey, err := x509.ParsePKCS1PrivateKey(uk.privKey.Private())
if err != nil {
return nil, fmt.Errorf("failed to parse root key: %v (%s)", gun, err.Error())
}
template, err := trustmanager.NewCertificate(gun)
if err != nil {
return nil, fmt.Errorf("failed to create the certificate template for: %s (%v)", gun, err)
}
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, privKey.Public(), privKey)
if err != nil {
return nil, fmt.Errorf("failed to create the certificate for: %s (%v)", gun, err)
}
// Encode the new certificate into PEM
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse the certificate for key: %s (%v)", gun, err)
}
return cert, nil
}

314
client/client_test.go Normal file
View File

@ -0,0 +1,314 @@
package client
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/docker/notary/trustmanager"
"github.com/endophage/gotuf/data"
"github.com/stretchr/testify/assert"
)
func createTestServer(t *testing.T) *httptest.Server {
// TUF will request /v2/docker.com/notary/_trust/tuf/timestamp.key
// Return a canned timestamp.key
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, `{"keytype":"ed25519","keyval":{"public":"y4wnCW7Y8NYCmKZyWqxxUUj8p7SSoV5Cr1Zc+jqBxBw=","private":null}}`)
}))
return ts
}
// TestInitRepo runs through the process of initializing a repository and makes
// sure the repository looks correct on disk.
func TestInitRepo(t *testing.T) {
gun := "docker.com/notary"
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
ts := createTestServer(t)
defer ts.Close()
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL)
assert.NoError(t, err, "error creating repo: %s", err)
rootKeyID, err := repo.GenRootKey("passphrase")
assert.NoError(t, err, "error generating root key: %s", err)
rootSigner, err := repo.GetRootSigner(rootKeyID, "passphrase")
assert.NoError(t, err, "error retreiving root key: %s", err)
err = repo.Initialize(rootSigner)
assert.NoError(t, err, "error creating repository: %s", err)
// Inspect contents of the temporary directory
expectedDirs := []string{
"private",
filepath.Join("private", gun),
filepath.Join("private", "root_keys"),
"trusted_certificates",
filepath.Join("trusted_certificates", gun),
"tuf",
filepath.Join("tuf", gun, "metadata"),
filepath.Join("tuf", gun, "targets"),
}
for _, dir := range expectedDirs {
fi, err := os.Stat(filepath.Join(tempBaseDir, dir))
assert.NoError(t, err, "missing directory in base directory: %s", dir)
assert.True(t, fi.Mode().IsDir(), "%s is not a directory", dir)
}
// Look for keys in private. The filenames should match the key IDs
// in the private key store.
privKeyList := repo.privKeyStore.ListFiles(true)
for _, privKeyName := range privKeyList {
_, err := os.Stat(privKeyName)
assert.NoError(t, err, "missing private key: %s", privKeyName)
}
// Look for keys in root_keys
// There should be a file named after the key ID of the root key we
// passed in.
rootKeyFilename := rootSigner.ID() + ".key"
_, err = os.Stat(filepath.Join(tempBaseDir, "private", "root_keys", rootKeyFilename))
assert.NoError(t, err, "missing root key")
// Also expect a symlink from the key ID of the certificate key to this
// root key
certificates := repo.certificateStore.GetCertificates()
assert.Len(t, certificates, 1, "unexpected number of certificates")
certID := trustmanager.FingerprintCert(certificates[0])
actualDest, err := os.Readlink(filepath.Join(tempBaseDir, "private", "root_keys", certID+".key"))
assert.NoError(t, err, "missing symlink to root key")
assert.Equal(t, rootKeyFilename, actualDest, "symlink to root key has wrong destination")
// There should be a trusted certificate
_, err = os.Stat(filepath.Join(tempBaseDir, "trusted_certificates", gun, certID+".crt"))
assert.NoError(t, err, "missing trusted certificate")
// Sanity check the TUF metadata files. Verify that they exist, the JSON is
// well-formed, and the signatures exist. For the root.json file, also check
// that the root, snapshot, and targets key IDs are present.
expectedTUFMetadataFiles := []string{
filepath.Join("tuf", gun, "metadata", "root.json"),
filepath.Join("tuf", gun, "metadata", "snapshot.json"),
filepath.Join("tuf", gun, "metadata", "targets.json"),
}
for _, filename := range expectedTUFMetadataFiles {
fullPath := filepath.Join(tempBaseDir, filename)
_, err := os.Stat(fullPath)
assert.NoError(t, err, "missing TUF metadata file: %s", filename)
jsonBytes, err := ioutil.ReadFile(fullPath)
assert.NoError(t, err, "error reading TUF metadata file %s: %s", filename, err)
var decoded data.Signed
err = json.Unmarshal(jsonBytes, &decoded)
assert.NoError(t, err, "error parsing TUF metadata file %s: %s", filename, err)
assert.Len(t, decoded.Signatures, 1, "incorrect number of signatures in TUF metadata file %s", filename)
assert.NotEmpty(t, decoded.Signatures[0].KeyID, "empty key ID field in TUF metadata file %s", filename)
assert.NotEmpty(t, decoded.Signatures[0].Method, "empty method field in TUF metadata file %s", filename)
assert.NotEmpty(t, decoded.Signatures[0].Signature, "empty signature in TUF metadata file %s", filename)
// Special case for root.json: also check that the signed
// content for keys and roles
if strings.HasSuffix(filename, "root.json") {
var decodedRoot data.Root
err := json.Unmarshal(decoded.Signed, &decodedRoot)
assert.NoError(t, err, "error parsing root.json signed section: %s", err)
assert.Equal(t, "Root", decodedRoot.Type, "_type mismatch in root.json")
// Expect 4 keys in the Keys map: root, targets, snapshot, timestamp
assert.Len(t, decodedRoot.Keys, 4, "wrong number of keys in root.json")
roleCount := 0
for role := range decodedRoot.Roles {
roleCount++
if role != "root" && role != "snapshot" && role != "targets" && role != "timestamp" {
t.Fatalf("unexpected role %s in root.json", role)
}
}
assert.Equal(t, 4, roleCount, "wrong number of roles (%d) in root.json", roleCount)
}
}
}
type tufChange struct {
// Abbreviated because Go doesn't permit a field and method of the same name
Actn int `json:"action"`
Role string `json:"role"`
ChangeType string `json:"type"`
ChangePath string `json:"path"`
Data []byte `json:"data"`
}
// TestAddTarget adds a target to the repo and confirms that the changelist
// is updated correctly.
func TestAddTarget(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts := createTestServer(t)
defer ts.Close()
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL)
assert.NoError(t, err, "error creating repository: %s", err)
rootKeyID, err := repo.GenRootKey("passphrase")
assert.NoError(t, err, "error generating root key: %s", err)
rootSigner, err := repo.GetRootSigner(rootKeyID, "passphrase")
assert.NoError(t, err, "error retreiving root key: %s", err)
err = repo.Initialize(rootSigner)
assert.NoError(t, err, "error creating repository: %s", err)
// Add fixtures/ca.cert as a target. There's no particular reason
// for using this file except that it happens to be available as
// a fixture.
target, err := NewTarget("latest", "../fixtures/ca.cert")
assert.NoError(t, err, "error creating target")
err = repo.AddTarget(target)
assert.NoError(t, err, "error adding target")
// Look for the changelist file
changelistDirPath := filepath.Join(tempBaseDir, "tuf", gun, "changelist")
changelistDir, err := os.Open(changelistDirPath)
assert.NoError(t, err, "could not open changelist directory")
fileInfos, err := changelistDir.Readdir(0)
assert.NoError(t, err, "could not read changelist directory")
// Should only be one file in the directory
assert.Len(t, fileInfos, 1, "wrong number of changelist files found")
clName := fileInfos[0].Name()
raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName))
assert.NoError(t, err, "could not read changelist file %s", clName)
c := &tufChange{}
err = json.Unmarshal(raw, c)
assert.NoError(t, err, "could not unmarshal changelist file %s", clName)
assert.EqualValues(t, 0, c.Actn)
assert.Equal(t, "targets", c.Role)
assert.Equal(t, "target", c.ChangeType)
assert.Equal(t, "latest", c.ChangePath)
assert.NotEmpty(t, c.Data)
changelistDir.Close()
// Create a second target
target, err = NewTarget("current", "../fixtures/ca.cert")
assert.NoError(t, err, "error creating target")
err = repo.AddTarget(target)
assert.NoError(t, err, "error adding target")
changelistDir, err = os.Open(changelistDirPath)
assert.NoError(t, err, "could not open changelist directory")
// There should now be a second file in the directory
fileInfos, err = changelistDir.Readdir(0)
assert.NoError(t, err, "could not read changelist directory")
assert.Len(t, fileInfos, 2, "wrong number of changelist files found")
newFileFound := false
for _, fileInfo := range fileInfos {
if fileInfo.Name() != clName {
clName2 := fileInfo.Name()
raw, err := ioutil.ReadFile(filepath.Join(changelistDirPath, clName2))
assert.NoError(t, err, "could not read changelist file %s", clName2)
c := &tufChange{}
err = json.Unmarshal(raw, c)
assert.NoError(t, err, "could not unmarshal changelist file %s", clName2)
assert.EqualValues(t, 0, c.Actn)
assert.Equal(t, "targets", c.Role)
assert.Equal(t, "target", c.ChangeType)
assert.Equal(t, "current", c.ChangePath)
assert.NotEmpty(t, c.Data)
newFileFound = true
break
}
}
assert.True(t, newFileFound, "second changelist file not found")
changelistDir.Close()
}
// TestValidateRootKey verifies that the public data in root.json for the root
// key is a valid x509 certificate.
func TestValidateRootKey(t *testing.T) {
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err, "failed to create a temporary directory: %s", err)
gun := "docker.com/notary"
ts := createTestServer(t)
defer ts.Close()
repo, err := NewNotaryRepository(tempBaseDir, gun, ts.URL)
assert.NoError(t, err, "error creating repository: %s", err)
rootKeyID, err := repo.GenRootKey("passphrase")
assert.NoError(t, err, "error generating root key: %s", err)
rootSigner, err := repo.GetRootSigner(rootKeyID, "passphrase")
assert.NoError(t, err, "error retreiving root key: %s", err)
err = repo.Initialize(rootSigner)
assert.NoError(t, err, "error creating repository: %s", err)
rootJSONFile := filepath.Join(tempBaseDir, "tuf", gun, "metadata", "root.json")
jsonBytes, err := ioutil.ReadFile(rootJSONFile)
assert.NoError(t, err, "error reading TUF metadata file %s: %s", rootJSONFile, err)
var decoded data.Signed
err = json.Unmarshal(jsonBytes, &decoded)
assert.NoError(t, err, "error parsing TUF metadata file %s: %s", rootJSONFile, err)
var decodedRoot data.Root
err = json.Unmarshal(decoded.Signed, &decodedRoot)
assert.NoError(t, err, "error parsing root.json signed section: %s", err)
keyids := []string{}
for role, roleData := range decodedRoot.Roles {
if role == "root" {
keyids = append(keyids, roleData.KeyIDs...)
}
}
assert.NotEmpty(t, keyids)
for _, keyid := range keyids {
if key, ok := decodedRoot.Keys[keyid]; !ok {
t.Fatal("key id not found in keys")
} else {
_, err := trustmanager.LoadCertFromPEM(key.Value.Public)
assert.NoError(t, err, "key is not a valid cert")
}
}
}

62
client/helpers.go Normal file
View File

@ -0,0 +1,62 @@
package client
import (
"encoding/json"
"time"
"github.com/docker/notary/client/changelist"
"github.com/endophage/gotuf"
"github.com/endophage/gotuf/data"
"github.com/endophage/gotuf/store"
)
// Use this to initialize remote HTTPStores from the config settings
func getRemoteStore(baseURL, gun string) (store.RemoteStore, error) {
return store.NewHTTPStore(
baseURL+"/v2/"+gun+"/_trust/tuf/",
"",
"json",
"",
"key",
)
}
func applyChangelist(repo *tuf.TufRepo, cl changelist.Changelist) error {
changes := cl.List()
var err error
for _, c := range changes {
if c.Scope() == "targets" {
applyTargetsChange(repo, c)
}
if err != nil {
return err
}
}
return nil
}
func applyTargetsChange(repo *tuf.TufRepo, c changelist.Change) error {
var err error
meta := &data.FileMeta{}
err = json.Unmarshal(c.Content(), meta)
if err != nil {
return nil
}
if c.Action() == changelist.ActionCreate {
files := data.Files{c.Path(): *meta}
_, err = repo.AddTargets("targets", files)
} else if c.Action() == changelist.ActionDelete {
err = repo.RemoveTargets("targets", c.Path())
}
if err != nil {
// TODO(endophage): print out rem entries as files that couldn't
// be added.
return err
}
return nil
}
func nearExpiry(r *data.SignedRoot) bool {
plus6mo := time.Now().AddDate(0, 6, 0)
return r.Signed.Expires.Before(plus6mo)
}

View File

@ -4,10 +4,11 @@
"tls_cert_file": "./fixtures/notary.pem", "tls_cert_file": "./fixtures/notary.pem",
"tls_key_file": "./fixtures/notary.key" "tls_key_file": "./fixtures/notary.key"
}, },
"trust_service":{ "trust_service": {
"type": "local", "type": "local",
"hostname": "", "hostname": "rufus",
"port": "" "port": "7899",
"tls_ca_file": "./fixtures/ca.cert"
}, },
"logging": { "logging": {
"level": 5 "level": 5

View File

@ -1,121 +0,0 @@
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"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
}
// NewCryptoService returns an instance ofS cliCryptoService
func NewCryptoService(gun string) *CliCryptoService {
return &CliCryptoService{privateKeys: make(map[string]*data.PrivateKey), gun: gun}
}
// 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)
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)
}
return data.NewPublicKey("RSA", pemdata), nil
}
// Sign returns the signatures for data with the given keyIDs
func (ccs *CliCryptoService) Sign(keyIDs []string, payload []byte) ([]data.Signature, error) {
// Create hasher and hash data
hash := crypto.SHA256
hashed := sha256.Sum256(payload)
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")
// Read PrivateKey from file
privPEMBytes, err := ioutil.ReadFile(privKeyFilename)
if err != nil {
continue
}
// Parse PrivateKey
privKeyBytes, _ := pem.Decode(privPEMBytes)
privKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes.Bytes)
if err != nil {
return nil, err
}
// Sign the data
sig, err := rsa.SignPKCS1v15(rand.Reader, privKey, hash, hashed[:])
if err != nil {
return nil, err
}
// Append signatures to result array
signatures = append(signatures, data.Signature{
KeyID: fingerprint,
Method: "RSASSA-PKCS1-V1_5-SIGN",
Signature: sig[:],
})
}
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) {
// Generates a new RSA key
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, nil, fmt.Errorf("could not generate private key: %v", err)
}
// Creates a new Certificate template. We need the certificate to calculate the
// TUF-compliant keyID
//TODO (diogo): We're hardcoding the Organization to be the GUN. Probably want to
// change it
template := 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)
}
// Encode the new certificate into PEM
cert, err := x509.ParseCertificate(derBytes)
if err != nil {
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)
}

View File

@ -1,12 +1,9 @@
package main package main
import ( import (
"crypto/rand"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"fmt" "fmt"
"math" "math"
"math/big"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
@ -95,7 +92,7 @@ func keysRemove(cmd *cobra.Command, args []string) {
} }
// We didn't find a certificate with this ID, let's try to see if we can find keys. // We didn't find a certificate with this ID, let's try to see if we can find keys.
keyList := privKeyStore.ListDir(gunOrID) keyList := privKeyStore.ListDir(gunOrID, true)
if len(keyList) < 1 { if len(keyList) < 1 {
fatalf("no Private Keys found under Global Unique Name: %s", gunOrID) fatalf("no Private Keys found under Global Unique Name: %s", gunOrID)
} }
@ -190,7 +187,7 @@ func keysList(cmd *cobra.Command, args []string) {
fmt.Println("") fmt.Println("")
fmt.Println("# Signing keys: ") fmt.Println("# Signing keys: ")
for _, k := range privKeyStore.ListAll() { for _, k := range privKeyStore.ListFiles(true) {
printKey(k) printKey(k)
} }
} }
@ -207,39 +204,14 @@ func keysGenerate(cmd *cobra.Command, args []string) {
fatalf("invalid Global Unique Name: %s", gun) fatalf("invalid Global Unique Name: %s", gun)
} }
_, cert, err := generateKeyAndCert(gun) // _, cert, err := generateKeyAndCert(gun)
if err != nil { // if err != nil {
fatalf("could not generate key: %v", err) // fatalf("could not generate key: %v", err)
} // }
certificateStore.AddCert(cert) // certificateStore.AddCert(cert)
fingerprint := trustmanager.FingerprintCert(cert) // fingerprint := trustmanager.FingerprintCert(cert)
fmt.Println("Generated new keypair with ID: ", fingerprint) // 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,
}
} }
func printCert(cert *x509.Certificate) { func printCert(cert *x509.Certificate) {

View File

@ -16,18 +16,15 @@ import (
) )
const configFileName string = "config" const configFileName string = "config"
// Default paths should end with a '/' so directory creation works correctly
const configPath string = ".docker/trust/" const configPath string = ".docker/trust/"
const trustDir string = configPath + "trusted_certificates/" const trustDir string = "trusted_certificates/"
const privDir string = configPath + "private/" const privDir string = "private/"
const tufDir string = configPath + "tuf/" const rootKeysDir string = "root_keys/"
var caStore trustmanager.X509Store
var certificateStore trustmanager.X509Store
var privKeyStore trustmanager.EncryptedFileStore
var rawOutput bool var rawOutput bool
var caStore trustmanager.X509Store
var certificateStore trustmanager.X509Store
var privKeyStore trustmanager.FileStore
func init() { func init() {
logrus.SetLevel(logrus.DebugLevel) logrus.SetLevel(logrus.DebugLevel)
@ -59,13 +56,11 @@ func init() {
} }
// Set up the defaults for our config // Set up the defaults for our config
viper.SetDefault("trustDir", path.Join(homeDir, path.Dir(trustDir))) viper.SetDefault("baseTrustDir", path.Join(homeDir, path.Dir(configPath)))
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 // Get the final value for the CA directory
finalTrustDir := viper.GetString("trustDir") finalTrustDir := path.Join(viper.GetString("baseTrustDir"), trustDir)
finalPrivDir := viper.GetString("privDir") finalPrivDir := path.Join(viper.GetString("baseTrustDir"), privDir)
// Load all CAs that aren't expired and don't use SHA1 // Load all CAs that aren't expired and don't use SHA1
caStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool { caStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool {
@ -76,10 +71,10 @@ func init() {
cert.SignatureAlgorithm != x509.ECDSAWithSHA1 cert.SignatureAlgorithm != x509.ECDSAWithSHA1
}) })
if err != nil { if err != nil {
fatalf("could not create X509FileStore: %v", err) fatalf("could not create CA 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 { certificateStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool {
return !cert.IsCA && return !cert.IsCA &&
time.Now().Before(cert.NotAfter) && time.Now().Before(cert.NotAfter) &&
@ -88,12 +83,12 @@ func init() {
cert.SignatureAlgorithm != x509.ECDSAWithSHA1 cert.SignatureAlgorithm != x509.ECDSAWithSHA1
}) })
if err != nil { if err != nil {
fatalf("could not create X509FileStore: %v", err) fatalf("could not create Certificate X509FileStore: %v", err)
} }
privKeyStore, err = trustmanager.NewKeyFileStore(finalPrivDir) privKeyStore, err = trustmanager.NewKeyFileStore(finalPrivDir)
if err != nil { if err != nil {
fatalf("could not create FileStore: %v", err) fatalf("could not create KeyFileStore: %v", err)
} }
} }
@ -108,13 +103,13 @@ func main() {
NotaryCmd.AddCommand(cmdKeys) NotaryCmd.AddCommand(cmdKeys)
NotaryCmd.AddCommand(cmdTufInit) NotaryCmd.AddCommand(cmdTufInit)
NotaryCmd.AddCommand(cmdTufList) 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(cmdTufAdd)
NotaryCmd.AddCommand(cmdTufRemove) NotaryCmd.AddCommand(cmdTufRemove)
NotaryCmd.AddCommand(cmdTufPublish) NotaryCmd.AddCommand(cmdTufPublish)
cmdTufPublish.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location") cmdTufPublish.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location")
NotaryCmd.AddCommand(cmdTufLookup) 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") cmdTufLookup.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location")
NotaryCmd.AddCommand(cmdVerify) NotaryCmd.AddCommand(cmdVerify)

View File

@ -1,29 +1,21 @@
package main package main
import ( import (
"bytes"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "errors"
"encoding/json"
"encoding/pem"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath"
"github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus"
"github.com/docker/notary/trustmanager" notaryclient "github.com/docker/notary/client"
"github.com/endophage/gotuf"
"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/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// FIXME: This should not be hardcoded
const hardcodedBaseURL = "https://notary:4443"
var remoteTrustServer string var remoteTrustServer string
var cmdTufList = &cobra.Command{ var cmdTufList = &cobra.Command{
@ -84,30 +76,21 @@ func tufAdd(cmd *cobra.Command, args []string) {
gun := args[0] gun := args[0]
targetName := args[1] targetName := args[1]
targetPath := args[2] targetPath := args[2]
kdb := keys.NewDB()
signer := signed.NewSigner(NewCryptoService(gun))
repo := tuf.NewTufRepo(kdb, signer)
b, err := ioutil.ReadFile(targetPath) repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
if err != nil { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
filestore := bootstrapRepo(gun, repo) target, err := notaryclient.NewTarget(targetName, targetPath)
fmt.Println("Generating metadata for target")
meta, err := data.NewFileMeta(bytes.NewBuffer(b))
if err != nil { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
err = repo.AddTarget(target)
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 { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
fmt.Println("Successfully added targets")
saveRepo(repo, filestore)
} }
func tufInit(cmd *cobra.Command, args []string) { func tufInit(cmd *cobra.Command, args []string) {
@ -117,91 +100,44 @@ func tufInit(cmd *cobra.Command, args []string) {
} }
gun := args[0] gun := args[0]
kdb := keys.NewDB()
signer := signed.NewSigner(NewCryptoService(gun))
remote, err := getRemoteStore(gun) nRepo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
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")
if err != nil { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
kdb.AddKey(rootKey) keysList := nRepo.ListRootKeys()
kdb.AddKey(targetsKey) var passphrase string
kdb.AddKey(snapshotKey) var rootKeyID string
kdb.AddKey(timestampKey) if len(keysList) < 1 {
fmt.Println("No root keys found. Generating a new root key...")
passphrase, err = passphraseRetriever()
if err != nil {
fatalf(err.Error())
}
rootKeyID, err = nRepo.GenRootKey(passphrase)
if err != nil {
fatalf(err.Error())
}
} else {
rootKeyID = keysList[0]
fmt.Println("Root key found.")
fmt.Printf("Enter passphrase for: %s (%d)\n", rootKeyID, len(rootKeyID))
passphrase, err = passphraseRetriever()
if err != nil {
fatalf(err.Error())
}
}
rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil) rootSigner, err := nRepo.GetRootSigner(rootKeyID, 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 { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
err = kdb.AddRole(rootRole) nRepo.Initialize(rootSigner)
if err != nil { if err != nil {
fatalf(err.Error()) 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)
} }
func tufList(cmd *cobra.Command, args []string) { func tufList(cmd *cobra.Command, args []string) {
@ -210,31 +146,21 @@ func tufList(cmd *cobra.Command, args []string) {
fatalf("must specify a GUN") fatalf("must specify a GUN")
} }
gun := args[0] gun := args[0]
kdb := keys.NewDB()
repo := tuf.NewTufRepo(kdb, nil)
remote, err := getRemoteStore(gun) repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
if err != nil { if err != nil {
return fatalf(err.Error())
}
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
} }
if rawOutput { // Retreive the remote list of signed targets
for name, meta := range repo.Targets["targets"].Signed.Targets { targetList, err := repo.ListTargets()
fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) if err != nil {
} fatalf(err.Error())
} else { }
for name, meta := range repo.Targets["targets"].Signed.Targets {
fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) // Print all the available targets
} for _, t := range targetList {
fmt.Println(t.Name, " ", t.Hashes["sha256"], " ", t.Length)
} }
} }
@ -245,29 +171,19 @@ func tufLookup(cmd *cobra.Command, args []string) {
} }
gun := args[0] gun := args[0]
targetName := args[1] targetName := args[1]
kdb := keys.NewDB()
repo := tuf.NewTufRepo(kdb, nil)
remote, err := getRemoteStore(gun) repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
c, err := bootstrapClient(gun, remote, repo, kdb)
if err != nil { 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 { if err != nil {
logrus.Error("Error updating client: ", err.Error()) fatalf(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)
} }
fmt.Println(target.Name, fmt.Sprintf("sha256:%s", target.Hashes["sha256"]), target.Length)
} }
func tufPublish(cmd *cobra.Command, args []string) { func tufPublish(cmd *cobra.Command, args []string) {
@ -277,41 +193,15 @@ func tufPublish(cmd *cobra.Command, args []string) {
} }
gun := args[0] gun := args[0]
fmt.Println("Pushing changes to ", gun, ".") fmt.Println("Pushing changes to ", gun, ".")
remote, err := getRemoteStore(gun) repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
filestore, err := store.NewFilesystemStore(
path.Join(viper.GetString("tufDir"), gun),
"metadata",
"json",
"targets",
)
if err != nil { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
root, err := filestore.GetMeta("root", 0) err = repo.Publish(passphraseRetriever)
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 { if err != nil {
fatalf(err.Error()) fatalf(err.Error())
} }
@ -324,20 +214,15 @@ func tufRemove(cmd *cobra.Command, args []string) {
} }
gun := args[0] gun := args[0]
targetName := args[1] targetName := args[1]
kdb := keys.NewDB()
signer := signed.NewSigner(NewCryptoService(gun))
repo := tuf.NewTufRepo(kdb, signer)
//c := changelist.NewTufChange(changelist.ActionDelete, "targets", "target", targetName, nil)
//err := cl.Add(c)
//if err != nil {
// fatalf(err.Error())
//}
// TODO(diogo): Implement RemoveTargets in libnotary
fmt.Println("Removing target ", targetName, " from ", gun) fmt.Println("Removing target ", targetName, " from ", gun)
filestore := bootstrapRepo(gun, repo)
err := repo.RemoveTargets("targets", targetName)
if err != nil {
fatalf(err.Error())
}
saveRepo(repo, filestore)
} }
func verify(cmd *cobra.Command, args []string) { func verify(cmd *cobra.Command, args []string) {
@ -356,31 +241,21 @@ func verify(cmd *cobra.Command, args []string) {
//TODO (diogo): This code is copy/pasted from lookup. //TODO (diogo): This code is copy/pasted from lookup.
gun := args[0] gun := args[0]
targetName := args[1] targetName := args[1]
kdb := keys.NewDB() repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL)
repo := tuf.NewTufRepo(kdb, nil)
remote, err := getRemoteStore(gun)
c, err := bootstrapClient(gun, remote, repo, kdb)
if err != nil { if err != nil {
logrus.Error("Unable to setup client.")
return
}
err = c.Update()
if err != nil {
fmt.Println("Update failed")
fatalf(err.Error()) 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.") logrus.Error("notary: data not present in the trusted collection.")
os.Exit(1) os.Exit(-11)
} }
// Create hasher and hash data // Create hasher and hash data
stdinHash := fmt.Sprintf("sha256:%x", sha256.Sum256(payload)) 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 { if stdinHash != serverHash {
logrus.Error("notary: data not present in the trusted collection.") logrus.Error("notary: data not present in the trusted collection.")
os.Exit(1) os.Exit(1)
@ -390,179 +265,16 @@ func verify(cmd *cobra.Command, args []string) {
return return
} }
func saveRepo(repo *tuf.TufRepo, filestore store.MetadataStore) error { func passphraseRetriever() (string, error) {
fmt.Println("Saving changes to Trusted Collection.") fmt.Println("Please provide a passphrase for this root key: ")
signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) var passphrase string
_, err := fmt.Scanln(&passphrase)
if err != nil { if err != nil {
return err return "", err
} }
rootJSON, _ := json.Marshal(signedRoot) if len(passphrase) < 8 {
filestore.SetMeta("root", rootJSON) fmt.Println("Please use a password manager to generate and store a good random passphrase.")
return "", errors.New("Passphrase too short")
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)
} }
return passphrase, nil
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(
"https://notary:4443/v2/"+gun+"/_trust/tuf/",
"",
"json",
"",
"key",
)
} }

197
q Normal file
View File

@ -0,0 +1,197 @@
diff --git a/client/client.go b/client/client.go
index 6916daf..8029996 100644
--- a/client/client.go
+++ b/client/client.go
@@ -618,7 +618,7 @@ func (r *NotaryRepository) ListRootKeys() []string {
func (r *NotaryRepository) GenRootKey(passphrase string) (string, error) {
privKey, err := trustmanager.GenerateRSAKey(rand.Reader, rsaRootKeySize)
if err != nil {
- return "", fmt.Errorf("failed to convert private key: ", err)
+ return "", fmt.Errorf("failed to convert private key: %v", err)
}

r.rootKeyStore.AddEncryptedKey(privKey.ID(), privKey, passphrase)
diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go
index af21933..7825170 100644
--- a/cmd/notary/tuf.go
+++ b/cmd/notary/tuf.go
@@ -2,14 +2,13 @@ package main

import (
"crypto/sha256"
+ "errors"
"fmt"
"io/ioutil"
"os"

"github.com/Sirupsen/logrus"
notaryclient "github.com/docker/notary/client"
- "github.com/endophage/gotuf/data"
- "github.com/endophage/gotuf/keys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
@@ -107,13 +106,30 @@ func tufInit(cmd *cobra.Command, args []string) {
fatalf(err.Error())
}

- // 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.
- rootKeyID, err := nRepo.GenRootKey("passphrase")
- if err != nil {
- fatalf(err.Error())
+ keysList := nRepo.ListRootKeys()
+ var passphrase string
+ var rootKeyID string
+ if len(keysList) < 1 {
+ fmt.Println("No root keys found. Generating a new root key...")
+ passphrase, err = passphraseRetriever()
+ if err != nil {
+ fatalf(err.Error())
+ }
+ rootKeyID, err = nRepo.GenRootKey(passphrase)
+ if err != nil {
+ fatalf(err.Error())
+ }
+ } else {
+ rootKeyID = keysList[0]
+ fmt.Println("Root key found.")
+ fmt.Printf("Enter passphrase for: %s (%d)\n", rootKeyID, len(rootKeyID))
+ passphrase, err = passphraseRetriever()
+ if err != nil {
+ fatalf(err.Error())
+ }
}
- rootSigner, err := nRepo.GetRootSigner(rootKeyID, "passphrase")
+
+ rootSigner, err := nRepo.GetRootSigner(rootKeyID, passphrase)
if err != nil {
fatalf(err.Error())
}
@@ -185,7 +201,7 @@ func tufPublish(cmd *cobra.Command, args []string) {
fatalf(err.Error())
}

- err = repo.Publish(passwordRetriever)
+ err = repo.Publish(passphraseRetriever)
if err != nil {
fatalf(err.Error())
}
@@ -249,76 +265,20 @@ func verify(cmd *cobra.Command, args []string) {
return
}

-//func generateKeys(kdb *keys.KeyDB, signer *signed.Signer, remote store.RemoteStore) (string, string, string, string, error) {
-// 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())
-//
-// rootKey, err := signer.Create("root")
-// if err != nil {
-// return "", "", "", "", err
-// }
-// targetsKey, err := signer.Create("targets")
-// if err != nil {
-// return "", "", "", "", err
-// }
-// snapshotKey, err := signer.Create("snapshot")
-// if err != nil {
-// return "", "", "", "", err
-// }
-//
-// kdb.AddKey(rootKey)
-// kdb.AddKey(targetsKey)
-// kdb.AddKey(snapshotKey)
-// kdb.AddKey(timestampKey)
-// return rootKey.ID(), targetsKey.ID(), snapshotKey.ID(), timestampKey.ID(), nil
-//}
-
-func generateRoles(kdb *keys.KeyDB, rootKeyID, targetsKeyID, snapshotKeyID, timestampKeyID string) error {
- rootRole, err := data.NewRole("root", 1, []string{rootKeyID}, nil, nil)
- if err != nil {
- return err
- }
- targetsRole, err := data.NewRole("targets", 1, []string{targetsKeyID}, nil, nil)
- if err != nil {
- return err
- }
- snapshotRole, err := data.NewRole("snapshot", 1, []string{snapshotKeyID}, nil, nil)
- if err != nil {
- return err
- }
- timestampRole, err := data.NewRole("timestamp", 1, []string{timestampKeyID}, nil, nil)
- if err != nil {
- return err
- }
+// func passwordRetriever() (string, error) {
+// return "passphrase", nil
+// }

- err = kdb.AddRole(rootRole)
+func passphraseRetriever() (string, error) {
+ fmt.Println("Please provide a passphrase for this root key: ")
+ var passphrase string
+ _, err := fmt.Scanln(&passphrase)
if err != nil {
- return err
+ return "", err
}
- err = kdb.AddRole(targetsRole)
- if err != nil {
- return err
- }
- err = kdb.AddRole(snapshotRole)
- if err != nil {
- return err
+ if len(passphrase) < 8 {
+ fmt.Println("Please use a password manager to generate and store a good random passphrase.")
+ return "", errors.New("Passphrase too short")
}
- err = kdb.AddRole(timestampRole)
- if err != nil {
- return err
- }
- return nil
-}
-
-func passwordRetriever() (string, error) {
- return "passphrase", nil
+ return passphrase, nil
}
diff --git a/trustmanager/keyfilestore.go b/trustmanager/keyfilestore.go
index 6418139..f076c79 100644
--- a/trustmanager/keyfilestore.go
+++ b/trustmanager/keyfilestore.go
@@ -1,6 +1,11 @@
package trustmanager

-import "github.com/endophage/gotuf/data"
+import (
+ "path/filepath"
+ "strings"
+
+ "github.com/endophage/gotuf/data"
+)

const (
keyExtension = "key"
@@ -79,5 +84,10 @@ func (s *KeyFileStore) GetDecryptedKey(name string, passphrase string) (*data.Pr
// There might be symlinks associating Certificate IDs to Public Keys, so this
// method only returns the IDs that aren't symlinks
func (s *KeyFileStore) ListKeys() []string {
- return s.ListFiles(false)
+ var keyIDList []string
+ for _, f := range s.ListFiles(false) {
+ keyID := strings.TrimSpace(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
+ keyIDList = append(keyIDList, keyID)
+ }
+ return keyIDList
}

View File

@ -35,7 +35,7 @@ func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *e
return nil return nil
} }
// AddHandler adds the provided json data for the role and GUN specified in the URL // UpdateHandler adds the provided json data for the role and GUN specified in the URL
func UpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { func UpdateHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError {
defer r.Body.Close() defer r.Body.Close()
s := ctx.Value("metaStore") s := ctx.Value("metaStore")
@ -102,6 +102,13 @@ func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *er
tufRole := vars["tufRole"] tufRole := vars["tufRole"]
out, err := store.GetCurrent(gun, tufRole) out, err := store.GetCurrent(gun, tufRole)
if err != nil { if err != nil {
if _, ok := err.(*storage.ErrNotFound); ok {
return &errors.HTTPError{
HTTPStatus: http.StatusNotFound,
Code: 9999,
Err: err,
}
}
logrus.Errorf("[Notary Server] 500 GET repository: %s, role: %s", gun, tufRole) logrus.Errorf("[Notary Server] 500 GET repository: %s, role: %s", gun, tufRole)
return &errors.HTTPError{ return &errors.HTTPError{
HTTPStatus: http.StatusInternalServerError, HTTPStatus: http.StatusInternalServerError,
@ -147,6 +154,7 @@ func DeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request)
return nil return nil
} }
// GetTimestampHandler returns a timestamp.json given a GUN
func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError {
s := ctx.Value("metaStore") s := ctx.Value("metaStore")
store, ok := s.(storage.MetaStore) store, ok := s.(storage.MetaStore)
@ -172,6 +180,13 @@ func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
out, err := timestamp.GetOrCreateTimestamp(gun, store, signer) out, err := timestamp.GetOrCreateTimestamp(gun, store, signer)
if err != nil { if err != nil {
if _, ok := err.(*storage.ErrNoKey); ok {
return &errors.HTTPError{
HTTPStatus: http.StatusNotFound,
Code: 9999,
Err: err,
}
}
return &errors.HTTPError{ return &errors.HTTPError{
HTTPStatus: http.StatusInternalServerError, HTTPStatus: http.StatusInternalServerError,
Code: 9999, Code: 9999,
@ -184,6 +199,8 @@ func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Req
return nil return nil
} }
// GetTimestampKeyHandler returns a timestamp public key, creating a new key-pair
// it if it doesn't yet exist
func GetTimestampKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { func GetTimestampKeyHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError {
s := ctx.Value("metaStore") s := ctx.Value("metaStore")
store, ok := s.(storage.MetaStore) store, ok := s.(storage.MetaStore)

View File

@ -58,7 +58,7 @@ func Run(ctx context.Context, addr, tlsCertFile, tlsKeyFile string, trust signed
lsnr = tls.NewListener(lsnr, tlsConfig) lsnr = tls.NewListener(lsnr, tlsConfig)
} }
var ac auth.AccessController = nil var ac auth.AccessController
hand := utils.RootHandlerFactory(ac, ctx, trust) hand := utils.RootHandlerFactory(ac, ctx, trust)
r := mux.NewRouter() r := mux.NewRouter()

View File

@ -27,13 +27,14 @@ type MySQLStorage struct {
sql.DB sql.DB
} }
// NewMySQLStorage is a convenience method to create a MySQLStorage
func NewMySQLStorage(db *sql.DB) *MySQLStorage { func NewMySQLStorage(db *sql.DB) *MySQLStorage {
return &MySQLStorage{ return &MySQLStorage{
DB: *db, DB: *db,
} }
} }
// Update multiple TUF records in a single transaction. // UpdateCurrent updates multiple TUF records in a single transaction.
// Always insert a new row. The unique constraint will ensure there is only ever // Always insert a new row. The unique constraint will ensure there is only ever
func (db *MySQLStorage) UpdateCurrent(gun, role string, version int, data []byte) error { func (db *MySQLStorage) UpdateCurrent(gun, role string, version int, data []byte) error {
checkStmt := "SELECT count(*) FROM `tuf_files` WHERE `gun`=? AND `role`=? AND `version`>=?;" checkStmt := "SELECT count(*) FROM `tuf_files` WHERE `gun`=? AND `role`=? AND `version`>=?;"
@ -62,7 +63,7 @@ func (db *MySQLStorage) UpdateCurrent(gun, role string, version int, data []byte
return nil return nil
} }
// Get a specific TUF record // GetCurrent gets a specific TUF record
func (db *MySQLStorage) GetCurrent(gun, tufRole string) (data []byte, err error) { func (db *MySQLStorage) GetCurrent(gun, tufRole string) (data []byte, err error) {
stmt := "SELECT `data` FROM `tuf_files` WHERE `gun`=? AND `role`=? ORDER BY `version` DESC LIMIT 1;" stmt := "SELECT `data` FROM `tuf_files` WHERE `gun`=? AND `role`=? ORDER BY `version` DESC LIMIT 1;"
rows, err := db.Query(stmt, gun, tufRole) // this should be a QueryRow() rows, err := db.Query(stmt, gun, tufRole) // this should be a QueryRow()
@ -82,11 +83,14 @@ func (db *MySQLStorage) GetCurrent(gun, tufRole string) (data []byte, err error)
return data, nil return data, nil
} }
// Delete deletes all the records for a specific GUN
func (db *MySQLStorage) Delete(gun string) error { func (db *MySQLStorage) Delete(gun string) error {
stmt := "DELETE FROM `tuf_files` WHERE `gun`=?;" stmt := "DELETE FROM `tuf_files` WHERE `gun`=?;"
_, err := db.Exec(stmt, gun) _, err := db.Exec(stmt, gun)
return err return err
} }
// GetTimestampKey returns the timestamps Public Key data
func (db *MySQLStorage) GetTimestampKey(gun string) (cipher string, public []byte, err error) { func (db *MySQLStorage) GetTimestampKey(gun string) (cipher string, public []byte, err error) {
stmt := "SELECT `cipher`, `public` FROM `timestamp_keys` WHERE `gun`=?;" stmt := "SELECT `cipher`, `public` FROM `timestamp_keys` WHERE `gun`=?;"
row := db.QueryRow(stmt, gun) row := db.QueryRow(stmt, gun)
@ -100,6 +104,8 @@ func (db *MySQLStorage) GetTimestampKey(gun string) (cipher string, public []byt
return cipher, public, err return cipher, public, err
} }
// SetTimestampKey attempts to write a TimeStamp key and returns an error if it already exists
func (db *MySQLStorage) SetTimestampKey(gun, cipher string, public []byte) error { func (db *MySQLStorage) SetTimestampKey(gun, cipher string, public []byte) error {
stmt := "INSERT INTO `timestamp_keys` (`gun`, `cipher`, `public`) VALUES (?,?,?);" stmt := "INSERT INTO `timestamp_keys` (`gun`, `cipher`, `public`) VALUES (?,?,?);"
_, err := db.Exec(stmt, gun, cipher, public) _, err := db.Exec(stmt, gun, cipher, public)

View File

@ -4,30 +4,38 @@ import (
"fmt" "fmt"
) )
// ErrOldVersion is returned when a newer version of TUF metadada is already available
type ErrOldVersion struct{} type ErrOldVersion struct{}
// ErrOldVersion is returned when a newer version of TUF metadada is already available
func (err ErrOldVersion) Error() string { func (err ErrOldVersion) Error() string {
return fmt.Sprintf("Error updating metadata. A newer version is already available") return fmt.Sprintf("Error updating metadata. A newer version is already available")
} }
// ErrNotFound is returned when TUF metadata isn't found for a specific record
type ErrNotFound struct{} type ErrNotFound struct{}
// Error implements error
func (err ErrNotFound) Error() string { func (err ErrNotFound) Error() string {
return fmt.Sprintf("No record found") return fmt.Sprintf("No record found")
} }
// ErrTimestampKeyExists is returned when a timestamp key already exists
type ErrTimestampKeyExists struct { type ErrTimestampKeyExists struct {
gun string gun string
} }
// ErrTimestampKeyExists is returned when a timestamp key already exists
func (err ErrTimestampKeyExists) Error() string { func (err ErrTimestampKeyExists) Error() string {
return fmt.Sprintf("Error, timestamp key already exists for %s", err.gun) return fmt.Sprintf("Error, timestamp key already exists for %s", err.gun)
} }
// ErrNoKey is returned when no timestamp key is found
type ErrNoKey struct { type ErrNoKey struct {
gun string gun string
} }
// ErrNoKey is returned when no timestamp key is found
func (err ErrNoKey) Error() string { func (err ErrNoKey) Error() string {
return fmt.Sprintf("Error, no timestamp key found for %s", err.gun) return fmt.Sprintf("Error, no timestamp key found for %s", err.gun)
} }

View File

@ -1,5 +1,6 @@
package storage package storage
// MetaStore holds the methods that are used for a Metadata Store
type MetaStore interface { type MetaStore interface {
UpdateCurrent(gun, role string, version int, data []byte) error UpdateCurrent(gun, role string, version int, data []byte) error
GetCurrent(gun, tufRole string) (data []byte, err error) GetCurrent(gun, tufRole string) (data []byte, err error)

View File

@ -16,23 +16,24 @@ type ver struct {
data []byte data []byte
} }
// memStorage is really just designed for dev and testing. It is very // MemStorage is really just designed for dev and testing. It is very
// inefficient in many scenarios // inefficient in many scenarios
type memStorage struct { type MemStorage struct {
lock sync.Mutex lock sync.Mutex
tufMeta map[string][]*ver tufMeta map[string][]*ver
tsKeys map[string]*key tsKeys map[string]*key
} }
// NewMemStorage instantiates a memStorage instance // NewMemStorage instantiates a memStorage instance
func NewMemStorage() *memStorage { func NewMemStorage() *MemStorage {
return &memStorage{ return &MemStorage{
tufMeta: make(map[string][]*ver), tufMeta: make(map[string][]*ver),
tsKeys: make(map[string]*key), tsKeys: make(map[string]*key),
} }
} }
func (st *memStorage) UpdateCurrent(gun, role string, version int, data []byte) error { // UpdateCurrent updates the meta data for a specific role
func (st *MemStorage) UpdateCurrent(gun, role string, version int, data []byte) error {
id := entryKey(gun, role) id := entryKey(gun, role)
st.lock.Lock() st.lock.Lock()
defer st.lock.Unlock() defer st.lock.Unlock()
@ -47,7 +48,8 @@ func (st *memStorage) UpdateCurrent(gun, role string, version int, data []byte)
return nil return nil
} }
func (st *memStorage) GetCurrent(gun, role string) (data []byte, err error) { // GetCurrent returns the metadada for a given role, under a GUN
func (st *MemStorage) GetCurrent(gun, role string) (data []byte, err error) {
id := entryKey(gun, role) id := entryKey(gun, role)
st.lock.Lock() st.lock.Lock()
defer st.lock.Unlock() defer st.lock.Unlock()
@ -58,10 +60,11 @@ func (st *memStorage) GetCurrent(gun, role string) (data []byte, err error) {
return space[len(st.tufMeta[id])-1].data, nil return space[len(st.tufMeta[id])-1].data, nil
} }
func (st *memStorage) Delete(gun string) error { // Delete delets all the metadata for a given GUN
func (st *MemStorage) Delete(gun string) error {
st.lock.Lock() st.lock.Lock()
defer st.lock.Unlock() defer st.lock.Unlock()
for k, _ := range st.tufMeta { for k := range st.tufMeta {
if strings.HasPrefix(k, gun) { if strings.HasPrefix(k, gun) {
delete(st.tufMeta, k) delete(st.tufMeta, k)
} }
@ -69,17 +72,20 @@ func (st *memStorage) Delete(gun string) error {
return nil return nil
} }
func (st *memStorage) GetTimestampKey(gun string) (cipher string, public []byte, err error) { // GetTimestampKey returns the public key material of the timestamp key of a given gun
func (st *MemStorage) GetTimestampKey(gun string) (cipher string, public []byte, err error) {
// no need for lock. It's ok to return nil if an update // no need for lock. It's ok to return nil if an update
// wasn't observed // wasn't observed
if k, ok := st.tsKeys[gun]; !ok { k, ok := st.tsKeys[gun]
if !ok {
return "", nil, &ErrNoKey{gun: gun} return "", nil, &ErrNoKey{gun: gun}
} else {
return k.cipher, k.public, nil
} }
return k.cipher, k.public, nil
} }
func (st *memStorage) SetTimestampKey(gun, cipher string, public []byte) error { // SetTimestampKey sets a Timestamp key under a gun
func (st *MemStorage) SetTimestampKey(gun, cipher string, public []byte) error {
k := &key{cipher: cipher, public: public} k := &key{cipher: cipher, public: public}
st.lock.Lock() st.lock.Lock()
defer st.lock.Unlock() defer st.lock.Unlock()

View File

@ -17,6 +17,7 @@ type RufusSigner struct {
sClient pb.SignerClient sClient pb.SignerClient
} }
// NewRufusSigner is a convinience method that returns RufusSigner
func NewRufusSigner(hostname string, port string, tlscafile string) *RufusSigner { func NewRufusSigner(hostname string, port string, tlscafile string) *RufusSigner {
var opts []grpc.DialOption var opts []grpc.DialOption
netAddr := net.JoinHostPort(hostname, port) netAddr := net.JoinHostPort(hostname, port)
@ -53,7 +54,7 @@ func (trust *RufusSigner) Sign(keyIDs []string, toSign []byte) ([]data.Signature
} }
signatures = append(signatures, data.Signature{ signatures = append(signatures, data.Signature{
KeyID: sig.KeyID.ID, KeyID: sig.KeyID.ID,
Method: sig.Algorithm, Method: sig.Algorithm.Algorithm,
Signature: sig.Content, Signature: sig.Content,
}) })
} }
@ -67,7 +68,7 @@ func (trust *RufusSigner) Create(role string) (*data.PublicKey, error) {
return nil, err return nil, err
} }
//TODO(mccauley): Update API to return algorithm and/or take it as a param //TODO(mccauley): Update API to return algorithm and/or take it as a param
public := data.NewPublicKey(publicKey.Algorithm, publicKey.PublicKey) public := data.NewPublicKey(publicKey.Algorithm.Algorithm, publicKey.PublicKey)
return public, nil return public, nil
} }
@ -81,7 +82,7 @@ func (trust *RufusSigner) PublicKeys(keyIDs ...string) (map[string]*data.PublicK
return nil, err return nil, err
} }
publicKeys[public.KeyID.ID] = publicKeys[public.KeyID.ID] =
data.NewPublicKey(public.Algorithm, public.PublicKey) data.NewPublicKey(public.Algorithm.Algorithm, public.PublicKey)
} }
return publicKeys, nil return publicKeys, nil
} }

View File

@ -17,14 +17,9 @@ type FileStore interface {
RemoveDir(directoryName string) error RemoveDir(directoryName string) error
Get(fileName string) ([]byte, error) Get(fileName string) ([]byte, error)
GetPath(fileName string) string GetPath(fileName string) string
ListAll() []string ListFiles(symlinks bool) []string
ListDir(directoryName string) []string ListDir(directoryName string, symlinks bool) []string
} Link(src, dst string) error
type EncryptedFileStore interface {
FileStore
AddEncrypted(fileName string, keyBytes []byte, passphrase string) error
GetDecrypted(fileName string, passphrase string) ([]byte, error)
} }
// SimpleFileStore implements FileStore // SimpleFileStore implements FileStore
@ -34,8 +29,8 @@ type SimpleFileStore struct {
perms os.FileMode perms os.FileMode
} }
// NewFileStore creates a directory with 755 permissions // NewSimpleFileStore creates a directory with 755 permissions
func NewFileStore(baseDir string, fileExt string) (FileStore, error) { func NewSimpleFileStore(baseDir string, fileExt string) (FileStore, error) {
if err := CreateDirectory(baseDir); err != nil { if err := CreateDirectory(baseDir); err != nil {
return nil, err return nil, err
} }
@ -47,8 +42,8 @@ func NewFileStore(baseDir string, fileExt string) (FileStore, error) {
}, nil }, nil
} }
// NewPrivateFileStore creates a directory with 700 permissions // NewPrivateSimpleFileStore creates a directory with 700 permissions
func NewPrivateFileStore(baseDir string, fileExt string) (FileStore, error) { func NewPrivateSimpleFileStore(baseDir string, fileExt string) (FileStore, error) {
if err := CreatePrivateDirectory(baseDir); err != nil { if err := CreatePrivateDirectory(baseDir); err != nil {
return nil, err return nil, err
} }
@ -108,19 +103,19 @@ func (f *SimpleFileStore) GetPath(name string) string {
return f.genFilePath(name) return f.genFilePath(name)
} }
// List lists all the files inside of a store // ListFiles lists all the files inside of a store
func (f *SimpleFileStore) ListAll() []string { func (f *SimpleFileStore) ListFiles(symlinks bool) []string {
return f.list(f.baseDir) return f.list(f.baseDir, symlinks)
} }
// List lists all the files inside of a directory identified by a name // ListDir lists all the files inside of a directory identified by a name
func (f *SimpleFileStore) ListDir(name string) []string { func (f *SimpleFileStore) ListDir(name string, symlinks bool) []string {
fullPath := filepath.Join(f.baseDir, name) fullPath := filepath.Join(f.baseDir, name)
return f.list(fullPath) return f.list(fullPath, symlinks)
} }
// list lists all the files in a directory given a full path // list lists all the files in a directory given a full path. Ignores symlinks.
func (f *SimpleFileStore) list(path string) []string { func (f *SimpleFileStore) list(path string, symlinks bool) []string {
files := make([]string, 0, 0) files := make([]string, 0, 0)
filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error { filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error {
// If there are errors, ignore this particular file // If there are errors, ignore this particular file
@ -131,6 +126,12 @@ func (f *SimpleFileStore) list(path string) []string {
if fi.IsDir() { if fi.IsDir() {
return nil return nil
} }
// If this is a symlink, and symlinks is true, ignore it
if !symlinks && fi.Mode()&os.ModeSymlink == os.ModeSymlink {
return nil
}
// Only allow matches that end with our certificate extension (e.g. *.crt) // Only allow matches that end with our certificate extension (e.g. *.crt)
matched, _ := filepath.Match("*"+f.fileExt, fi.Name()) matched, _ := filepath.Match("*"+f.fileExt, fi.Name())
@ -144,10 +145,26 @@ func (f *SimpleFileStore) list(path string) []string {
// genFilePath returns the full path with extension given a file name // genFilePath returns the full path with extension given a file name
func (f *SimpleFileStore) genFilePath(name string) string { func (f *SimpleFileStore) genFilePath(name string) string {
fileName := fmt.Sprintf("%s.%s", name, f.fileExt) fileName := f.genFileName(name)
return filepath.Join(f.baseDir, fileName) return filepath.Join(f.baseDir, fileName)
} }
// genFileName returns the name using the right extension
func (f *SimpleFileStore) genFileName(name string) string {
return fmt.Sprintf("%s.%s", name, f.fileExt)
}
// Link creates a symlink beetween the ID of the certificate used by a repository
// and the ID of the root key that is being used.
// We use full path for the source and local for the destination to use relative
// path for the symlink
func (f *SimpleFileStore) Link(oldname, newname string) error {
return os.Symlink(
f.genFileName(oldname),
f.genFilePath(newname),
)
}
// CreateDirectory uses createDirectory to create a chmod 755 Directory // CreateDirectory uses createDirectory to create a chmod 755 Directory
func CreateDirectory(dir string) error { func CreateDirectory(dir string) error {
return createDirectory(dir, visible) return createDirectory(dir, visible)

View File

@ -129,7 +129,7 @@ func TestRemoveDir(t *testing.T) {
} }
} }
func TestListAll(t *testing.T) { func TestListFiles(t *testing.T) {
testName := "docker.com/notary/certificate" testName := "docker.com/notary/certificate"
testExt := "crt" testExt := "crt"
perms := os.FileMode(0755) perms := os.FileMode(0755)
@ -144,11 +144,18 @@ func TestListAll(t *testing.T) {
// Create 10 randomfiles // Create 10 randomfiles
for i := 1; i <= 10; i++ { for i := 1; i <= 10; i++ {
// Since we're generating this manually we need to add the extension '.' // Since we're generating this manually we need to add the extension '.'
expectedFilePath = filepath.Join(tempBaseDir, testName+string(i)+"."+testExt) expectedFilename := testName + strconv.Itoa(i) + "." + testExt
expectedFilePath = filepath.Join(tempBaseDir, expectedFilename)
_, err = generateRandomFile(expectedFilePath, perms) _, err = generateRandomFile(expectedFilePath, perms)
if err != nil { if err != nil {
t.Fatalf("failed to generate random file: %v", err) t.Fatalf("failed to generate random file: %v", err)
} }
// Create symlinks for all the files
err = os.Symlink(expectedFilename, filepath.Join(tempBaseDir, expectedFilename+".link."+testExt))
if err != nil {
t.Fatalf("failed to create symlink: %v", err)
}
} }
// Create our SimpleFileStore // Create our SimpleFileStore
@ -158,11 +165,17 @@ func TestListAll(t *testing.T) {
perms: perms, perms: perms,
} }
// Call the List function // Call the List function. Expect 10 real files when not listing symlinks
files := store.ListAll() files := store.ListFiles(false)
if len(files) != 10 { if len(files) != 10 {
t.Fatalf("expected 10 files in listing, got: %d", len(files)) t.Fatalf("expected 10 files in listing, got: %d", len(files))
} }
// Call the List function. Expect 20 total files when listing symlinks
files = store.ListFiles(true)
if len(files) != 20 {
t.Fatalf("expected 20 files in listing, got: %d", len(files))
}
} }
func TestListDir(t *testing.T) { func TestListDir(t *testing.T) {
@ -196,19 +209,64 @@ func TestListDir(t *testing.T) {
} }
// Call the ListDir function // Call the ListDir function
files := store.ListDir("docker.com/") files := store.ListDir("docker.com/", true)
if len(files) != 10 { if len(files) != 10 {
t.Fatalf("expected 10 files in listing, got: %d", len(files)) t.Fatalf("expected 10 files in listing, got: %d", len(files))
} }
files = store.ListDir("docker.com/notary") files = store.ListDir("docker.com/notary", true)
if len(files) != 10 { if len(files) != 10 {
t.Fatalf("expected 10 files in listing, got: %d", len(files)) t.Fatalf("expected 10 files in listing, got: %d", len(files))
} }
files = store.ListDir("fakedocker.com/") files = store.ListDir("fakedocker.com/", true)
if len(files) != 0 { if len(files) != 0 {
t.Fatalf("expected 0 files in listing, got: %d", len(files)) t.Fatalf("expected 0 files in listing, got: %d", len(files))
} }
} }
func TestLink(t *testing.T) {
testName := "docker.com/notary/certificate"
testSymlink := "docker.com/notary/certificate-symlink"
testExt := "crt"
perms := os.FileMode(0755)
// Temporary directory where test files will be created
tempBaseDir, err := ioutil.TempDir("", "notary-test-")
if err != nil {
t.Fatalf("failed to create a temporary directory: %v", err)
}
// Since we're generating this manually we need to add the extension '.'
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
expectedSymlinkPath := filepath.Join(tempBaseDir, testSymlink+"."+testExt)
_, err = generateRandomFile(expectedFilePath, perms)
if err != nil {
t.Fatalf("failed to generate random file: %v", err)
}
// Create our SimpleFileStore
store := &SimpleFileStore{
baseDir: tempBaseDir,
fileExt: testExt,
perms: perms,
}
// Call the Link function
err = store.Link("certificate", testSymlink)
if err != nil {
t.Fatalf("failed to create symlink: %v", err)
}
// Check to see if the symlink exists
actualSymlinkDest, err := os.Readlink(expectedSymlinkPath)
if err != nil {
t.Fatalf("expected to find symlink at path: %s", expectedSymlinkPath)
}
if actualSymlinkDest != "certificate."+testExt {
t.Fatalf("symlink has wrong destination: %s", actualSymlinkDest)
}
}
func TestGetPath(t *testing.T) { func TestGetPath(t *testing.T) {
testExt := "crt" testExt := "crt"
perms := os.FileMode(0755) perms := os.FileMode(0755)

View File

@ -1,6 +1,11 @@
package trustmanager package trustmanager
import "errors" import (
"path/filepath"
"strings"
"github.com/endophage/gotuf/data"
)
const ( const (
keyExtension = "key" keyExtension = "key"
@ -14,7 +19,7 @@ type KeyFileStore struct {
// NewKeyFileStore returns a new KeyFileStore creating a private directory to // NewKeyFileStore returns a new KeyFileStore creating a private directory to
// hold the keys. // hold the keys.
func NewKeyFileStore(baseDir string) (*KeyFileStore, error) { func NewKeyFileStore(baseDir string) (*KeyFileStore, error) {
fileStore, err := NewFileStore(baseDir, keyExtension) fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -22,35 +27,67 @@ func NewKeyFileStore(baseDir string) (*KeyFileStore, error) {
return &KeyFileStore{fileStore}, nil return &KeyFileStore{fileStore}, nil
} }
// AddEncrypted stores the contents of a PEM-encoded private key as an encrypted PEM block // AddKey stores the contents of a PEM-encoded private key as a PEM block
func (s *KeyFileStore) AddEncrypted(fileName string, pemKey []byte, passphrase string) error { func (s *KeyFileStore) AddKey(name string, privKey *data.PrivateKey) error {
pemPrivKey, err := KeyToPEM(privKey)
privKey, err := ParsePEMPrivateKey(pemKey)
if err != nil { if err != nil {
return err return err
} }
encryptedKey, err := EncryptPrivateKey(privKey, passphrase) return s.Add(name, pemPrivKey)
if err != nil {
return err
}
return s.Add(fileName, encryptedKey)
} }
// GetDecrypted decrypts and returns the PEM Encoded private key given a flename // GetKey returns the PrivateKey given a KeyID
// and a passphrase func (s *KeyFileStore) GetKey(name string) (*data.PrivateKey, error) {
func (s *KeyFileStore) GetDecrypted(fileName string, passphrase string) ([]byte, error) { keyBytes, err := s.Get(name)
keyBytes, err := s.Get(fileName)
if err != nil {
return nil, errors.New("could not retrieve private key material")
}
// Gets an unencrypted PrivateKey.
privKey, err := ParsePEMEncryptedPrivateKey(keyBytes, passphrase)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return KeyToPEM(privKey) // Convert PEM encoded bytes back to a PrivateKey
privKey, err := ParsePEMPrivateKey(keyBytes, "")
if err != nil {
return nil, err
}
return privKey, nil
}
// AddEncryptedKey stores the contents of a PEM-encoded private key as an encrypted PEM block
func (s *KeyFileStore) AddEncryptedKey(name string, privKey *data.PrivateKey, passphrase string) error {
encryptedPrivKey, err := EncryptPrivateKey(privKey, passphrase)
if err != nil {
return err
}
return s.Add(name, encryptedPrivKey)
}
// GetDecryptedKey decrypts and returns the PEM Encoded private key given a flename
// and a passphrase
func (s *KeyFileStore) GetDecryptedKey(name string, passphrase string) (*data.PrivateKey, error) {
keyBytes, err := s.Get(name)
if err != nil {
return nil, err
}
// Gets an unencrypted PrivateKey.
privKey, err := ParsePEMPrivateKey(keyBytes, passphrase)
if err != nil {
return nil, err
}
return privKey, nil
}
// ListKeys returns a list of unique PublicKeys present on the KeyFileStore.
// There might be symlinks associating Certificate IDs to Public Keys, so this
// method only returns the IDs that aren't symlinks
func (s *KeyFileStore) ListKeys() []string {
var keyIDList []string
for _, f := range s.ListFiles(false) {
keyID := strings.TrimSpace(strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)))
keyIDList = append(keyIDList, keyID)
}
return keyIDList
} }

View File

@ -3,7 +3,6 @@ package trustmanager
import ( import (
"bytes" "bytes"
"crypto/rand" "crypto/rand"
"crypto/rsa"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -30,19 +29,13 @@ func TestAddKey(t *testing.T) {
t.Fatalf("failed to create new key filestore: %v", err) t.Fatalf("failed to create new key filestore: %v", err)
} }
key, err := rsa.GenerateKey(rand.Reader, 1024) privKey, err := GenerateRSAKey(rand.Reader, 512)
if err != nil { if err != nil {
t.Fatalf("could not generate private key: %v", err) t.Fatalf("could not generate private key: %v", err)
} }
// Get the PEM for the key // Call the AddKey function
pemKey, err := KeyToPEM(key) err = store.AddKey(testName, privKey)
if err != nil {
t.Fatalf("failed to convert private key to PEM: %v", err)
}
// Call the Add function
err = store.Add(testName, pemKey)
if err != nil { if err != nil {
t.Fatalf("failed to add file to store: %v", err) t.Fatalf("failed to add file to store: %v", err)
} }
@ -111,19 +104,23 @@ EMl3eFOJXjIch/wIesRSN+2dGOsl7neercjMh1i9RvpCwHDx/E0=
t.Fatalf("failed to create new key filestore: %v", err) t.Fatalf("failed to create new key filestore: %v", err)
} }
// Call the Get function // Call the GetKey function
pemKey, err := store.Get(testName) privKey, err := store.GetKey(testName)
if err != nil { if err != nil {
t.Fatalf("failed to get file from store: %v", err) t.Fatalf("failed to get file from store: %v", err)
} }
if !bytes.Equal(testData, pemKey) { pemPrivKey, err := KeyToPEM(privKey)
if err != nil {
t.Fatalf("failed to convert key to PEM: %v", err)
}
if !bytes.Equal(testData, pemPrivKey) {
t.Fatalf("unexpected content in the file: %s", filePath) t.Fatalf("unexpected content in the file: %s", filePath)
} }
} }
func TestAddEncryptedAndGetDecrypted(t *testing.T) { func TestAddEncryptedAndGetDecrypted(t *testing.T) {
testName := "docker.com/notary/root"
testExt := "key" testExt := "key"
// Temporary directory where test files will be created // Temporary directory where test files will be created
@ -132,9 +129,6 @@ func TestAddEncryptedAndGetDecrypted(t *testing.T) {
t.Fatalf("failed to create a temporary directory: %v", err) t.Fatalf("failed to create a temporary directory: %v", err)
} }
// Since we're generating this manually we need to add the extension '.'
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
// Create our FileStore // Create our FileStore
store, err := NewKeyFileStore(tempBaseDir) store, err := NewKeyFileStore(tempBaseDir)
if err != nil { if err != nil {
@ -142,35 +136,38 @@ func TestAddEncryptedAndGetDecrypted(t *testing.T) {
} }
// Generate new PrivateKey // Generate new PrivateKey
key, err := rsa.GenerateKey(rand.Reader, 1024) privKey, err := GenerateRSAKey(rand.Reader, 512)
if err != nil { if err != nil {
t.Fatalf("could not generate private key: %v", err) t.Fatalf("could not generate private key: %v", err)
} }
// Get PEM encodedd key // Call the AddEncryptedKey function
pemKey, err := KeyToPEM(key) err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica")
if err != nil {
t.Fatalf("Could not encode key to PEM: %v", err)
}
// Call the Add function
err = store.AddEncrypted(testName, pemKey, "diogomonica")
if err != nil { if err != nil {
t.Fatalf("failed to add file to store: %v", err) t.Fatalf("failed to add file to store: %v", err)
} }
pemPrivKey, err := store.GetDecrypted(testName, "diogomonica") // Since we're generating this manually we need to add the extension '.'
expectedFilePath := filepath.Join(tempBaseDir, privKey.ID()+"."+testExt)
// Check to see if file exists
_, err = ioutil.ReadFile(expectedFilePath)
if err != nil {
t.Fatalf("expected file not found: %v", err)
}
// Call the GetDecryptedKey function
readPrivKey, err := store.GetDecryptedKey(privKey.ID(), "diogomonica")
if err != nil { if err != nil {
t.Fatalf("could not decrypt private key: %v", err) t.Fatalf("could not decrypt private key: %v", err)
} }
if !strings.Contains(string(pemKey), string(pemPrivKey)) { if !bytes.Equal(privKey.Private(), readPrivKey.Private()) {
t.Fatalf("expected private key content in the file: %s", expectedFilePath) t.Fatalf("written key and loaded key do not match")
} }
} }
func TestGetDecryptedWithTamperedCipherText(t *testing.T) { func TestGetDecryptedWithTamperedCipherText(t *testing.T) {
testName := "docker.com/notary/root"
testExt := "key" testExt := "key"
// Temporary directory where test files will be created // Temporary directory where test files will be created
@ -179,9 +176,6 @@ func TestGetDecryptedWithTamperedCipherText(t *testing.T) {
t.Fatalf("failed to create a temporary directory: %v", err) t.Fatalf("failed to create a temporary directory: %v", err)
} }
// Since we're generating this manually we need to add the extension '.'
expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt)
// Create our FileStore // Create our FileStore
store, err := NewKeyFileStore(tempBaseDir) store, err := NewKeyFileStore(tempBaseDir)
if err != nil { if err != nil {
@ -189,24 +183,22 @@ func TestGetDecryptedWithTamperedCipherText(t *testing.T) {
} }
// Generate a new Private Key // Generate a new Private Key
key, err := rsa.GenerateKey(rand.Reader, 1024) privKey, err := GenerateRSAKey(rand.Reader, 512)
if err != nil { if err != nil {
t.Fatalf("could not generate private key: %v", err) t.Fatalf("could not generate private key: %v", err)
} }
// Get PEM encodedd key // Call the AddEncryptedKey function
pemKey, err := KeyToPEM(key) err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica")
if err != nil {
t.Fatalf("Could not encode key to PEM: %v", err)
}
// Call the Add function
err = store.AddEncrypted(testName, pemKey, "diogomonica")
if err != nil { if err != nil {
t.Fatalf("failed to add file to store: %v", err) t.Fatalf("failed to add file to store: %v", err)
} }
// Since we're generating this manually we need to add the extension '.'
expectedFilePath := filepath.Join(tempBaseDir, privKey.ID()+"."+testExt)
// Get file description, open file // Get file description, open file
fp, _ := os.OpenFile(expectedFilePath, os.O_WRONLY, 0600) fp, err := os.OpenFile(expectedFilePath, os.O_WRONLY, 0600)
if err != nil { if err != nil {
t.Fatalf("expected file not found: %v", err) t.Fatalf("expected file not found: %v", err)
} }
@ -215,7 +207,7 @@ func TestGetDecryptedWithTamperedCipherText(t *testing.T) {
fp.WriteAt([]byte("a"), int64(1)) fp.WriteAt([]byte("a"), int64(1))
// Try to decrypt the file // Try to decrypt the file
_, err = store.GetDecrypted(testName, "diogomonica") _, err = store.GetDecryptedKey(privKey.ID(), "diogomonica")
if err == nil { if err == nil {
t.Fatalf("expected error while decrypting the content due to invalid cipher text") t.Fatalf("expected error while decrypting the content due to invalid cipher text")
} }
@ -237,24 +229,19 @@ func TestGetDecryptedWithInvalidPassphrase(t *testing.T) {
} }
// Generate a new random RSA Key // Generate a new random RSA Key
key, err := rsa.GenerateKey(rand.Reader, 1024) privKey, err := GenerateRSAKey(rand.Reader, 512)
if err != nil { if err != nil {
t.Fatalf("could not generate private key: %v", err) t.Fatalf("could not generate private key: %v", err)
} }
// Get PEM encodedd key // Call the AddEncryptedKey function
pemKey, err := KeyToPEM(key) err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica")
if err != nil {
t.Fatalf("Could not encode key to PEM: %v", err)
}
// Call the Add function
err = store.AddEncrypted(testName, pemKey, "diogomonica")
if err != nil { if err != nil {
t.Fatalf("failed to add file to stoAFre: %v", err) t.Fatalf("failed to add file to stoAFre: %v", err)
} }
// Try to decrypt the file with an invalid passphrase // Try to decrypt the file with an invalid passphrase
_, err = store.GetDecrypted(testName, "diegomonica") _, err = store.GetDecryptedKey(testName, "diegomonica")
if err == nil { if err == nil {
t.Fatalf("expected error while decrypting the content due to invalid passphrase") t.Fatalf("expected error while decrypting the content due to invalid passphrase")
} }

View File

@ -5,6 +5,8 @@ import (
"errors" "errors"
"os" "os"
"path" "path"
"github.com/Sirupsen/logrus"
) )
// X509FileStore implements X509Store that persists on disk // X509FileStore implements X509Store that persists on disk
@ -29,7 +31,7 @@ func NewX509FilteredFileStore(directory string, validate func(*x509.Certificate)
} }
func newX509FileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) { func newX509FileStore(directory string, validate func(*x509.Certificate) bool) (*X509FileStore, error) {
fileStore, err := NewFileStore(directory, certExtension) fileStore, err := NewSimpleFileStore(directory, certExtension)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,7 +71,7 @@ func (s X509FileStore) AddCert(cert *x509.Certificate) error {
// stored under. If the file does not exist on disk, saves it. // stored under. If the file does not exist on disk, saves it.
func (s X509FileStore) addNamedCert(cert *x509.Certificate) error { func (s X509FileStore) addNamedCert(cert *x509.Certificate) error {
fingerprint := fingerprintCert(cert) fingerprint := fingerprintCert(cert)
logrus.Debug("Adding cert with fingerprint: ", fingerprint)
// Validate if we already loaded this certificate before // Validate if we already loaded this certificate before
if _, ok := s.fingerprintMap[fingerprint]; ok { if _, ok := s.fingerprintMap[fingerprint]; ok {
return errors.New("certificate already in the store") return errors.New("certificate already in the store")
@ -81,10 +83,12 @@ func (s X509FileStore) addNamedCert(cert *x509.Certificate) error {
fileName := fileName(cert) fileName := fileName(cert)
// Save the file to disk if not already there. // Save the file to disk if not already there.
if _, err := os.Stat(fileName); os.IsNotExist(err) { if _, err := os.Stat(s.fileStore.GetPath(fileName)); os.IsNotExist(err) {
if err := s.fileStore.Add(fileName, certBytes); err != nil { if err := s.fileStore.Add(fileName, certBytes); err != nil {
return err return err
} }
} else if err != nil {
return err
} }
// We wrote the certificate succcessfully, add it to our in-memory storage // We wrote the certificate succcessfully, add it to our in-memory storage
@ -131,7 +135,7 @@ func (s X509FileStore) RemoveCert(cert *x509.Certificate) error {
// AddCertFromPEM adds the first certificate that it finds in the byte[], returning // AddCertFromPEM adds the first certificate that it finds in the byte[], returning
// an error if no Certificates are found // an error if no Certificates are found
func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error {
cert, err := loadCertFromPEM(pemBytes) cert, err := LoadCertFromPEM(pemBytes)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,16 +1,19 @@
package trustmanager package trustmanager
import ( import (
"crypto"
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/pem" "encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"math/big"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/endophage/gotuf/data" "github.com/endophage/gotuf/data"
) )
@ -41,7 +44,7 @@ func GetCertFromURL(urlStr string) (*x509.Certificate, error) {
} }
// Try to extract the first valid PEM certificate from the bytes // Try to extract the first valid PEM certificate from the bytes
cert, err := loadCertFromPEM(certBytes) cert, err := LoadCertFromPEM(certBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -56,34 +59,30 @@ func CertToPEM(cert *x509.Certificate) []byte {
return pemCert return pemCert
} }
// KeyToPEM returns a PEM encoded key from a crypto.PrivateKey // KeyToPEM returns a PEM encoded key from a Private Key
func KeyToPEM(key crypto.PrivateKey) ([]byte, error) { func KeyToPEM(privKey *data.PrivateKey) ([]byte, error) {
rsaKey, ok := key.(*rsa.PrivateKey) if privKey.Cipher() != "RSA" {
if !ok {
return nil, errors.New("only RSA keys are currently supported") return nil, errors.New("only RSA keys are currently supported")
} }
keyBytes := x509.MarshalPKCS1PrivateKey(rsaKey) return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privKey.Private()}), nil
return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes}), nil
} }
// EncryptPrivateKey returns an encrypted PEM encoded key given a Private key // EncryptPrivateKey returns an encrypted PEM key given a Privatekey
// and a passphrase // and a passphrase
func EncryptPrivateKey(key crypto.PrivateKey, passphrase string) ([]byte, error) { func EncryptPrivateKey(key *data.PrivateKey, passphrase string) ([]byte, error) {
rsaKey, ok := key.(*rsa.PrivateKey) // TODO(diogo): Currently only supports RSA Private keys
if !ok { if key.Cipher() != "RSA" {
return nil, errors.New("only RSA keys are currently supported") return nil, errors.New("only RSA keys are currently supported")
} }
keyBytes := x509.MarshalPKCS1PrivateKey(rsaKey)
password := []byte(passphrase) password := []byte(passphrase)
cipherType := x509.PEMCipherAES256 cipherType := x509.PEMCipherAES256
blockType := "RSA PRIVATE KEY" blockType := "RSA PRIVATE KEY"
encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader,
blockType, blockType,
keyBytes, key.Private(),
password, password,
cipherType) cipherType)
if err != nil { if err != nil {
@ -93,9 +92,9 @@ func EncryptPrivateKey(key crypto.PrivateKey, passphrase string) ([]byte, error)
return pem.EncodeToMemory(encryptedPEMBlock), nil return pem.EncodeToMemory(encryptedPEMBlock), nil
} }
// loadCertFromPEM returns the first certificate found in a bunch of bytes or error // LoadCertFromPEM returns the first certificate found in a bunch of bytes or error
// if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85. // if nothing is found. Taken from https://golang.org/src/crypto/x509/cert_pool.go#L85.
func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) {
for len(pemBytes) > 0 { for len(pemBytes) > 0 {
var block *pem.Block var block *pem.Block
block, pemBytes = pem.Decode(pemBytes) block, pemBytes = pem.Decode(pemBytes)
@ -134,7 +133,7 @@ func fingerprintCert(cert *x509.Certificate) CertID {
// loadCertsFromDir receives a store AddCertFromFile for each certificate found // loadCertsFromDir receives a store AddCertFromFile for each certificate found
func loadCertsFromDir(s *X509FileStore) { func loadCertsFromDir(s *X509FileStore) {
certFiles := s.fileStore.ListAll() certFiles := s.fileStore.ListFiles(true)
for _, c := range certFiles { for _, c := range certFiles {
s.AddCertFromFile(c) s.AddCertFromFile(c)
} }
@ -161,39 +160,9 @@ func LoadCertFromFile(filename string) (*x509.Certificate, error) {
return nil, errors.New("could not load certificate from file") return nil, errors.New("could not load certificate from file")
} }
// LoadKeyFromFile returns a PrivateKey given a filename // ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It
func LoadKeyFromFile(filename string) (crypto.PrivateKey, error) { // only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted.
pemBytes, err := ioutil.ReadFile(filename) func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (*data.PrivateKey, error) {
if err != nil {
return nil, err
}
key, err := ParsePEMPrivateKey(pemBytes)
if err != nil {
return nil, err
}
return key, nil
}
// ParsePEMPrivateKey returns a private key from a PEM encoded private key. It
// only supports RSA (PKCS#1).
func ParsePEMPrivateKey(pemBytes []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("no valid key found")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("unsupported key type %q", block.Type)
}
}
// ParsePEMEncryptedPrivateKey returns a private key from a PEM encrypted private key. It
// only supports RSA (PKCS#1).
func ParsePEMEncryptedPrivateKey(pemBytes []byte, passphrase string) (crypto.PrivateKey, error) {
block, _ := pem.Decode(pemBytes) block, _ := pem.Decode(pemBytes)
if block == nil { if block == nil {
return nil, errors.New("no valid private key found") return nil, errors.New("no valid private key found")
@ -201,17 +170,82 @@ func ParsePEMEncryptedPrivateKey(pemBytes []byte, passphrase string) (crypto.Pri
switch block.Type { switch block.Type {
case "RSA PRIVATE KEY": case "RSA PRIVATE KEY":
if !x509.IsEncryptedPEMBlock(block) { var privKeyBytes []byte
return nil, errors.New("private key is not encrypted") var err error
if x509.IsEncryptedPEMBlock(block) {
privKeyBytes, err = x509.DecryptPEMBlock(block, []byte(passphrase))
if err != nil {
return nil, errors.New("could not decrypt private key")
}
} else {
privKeyBytes = block.Bytes
} }
decryptedPEMBlock, err := x509.DecryptPEMBlock(block, []byte(passphrase)) rsaPrivKey, err := x509.ParsePKCS1PrivateKey(privKeyBytes)
if err != nil { if err != nil {
return nil, errors.New("could not decrypt private key") return nil, fmt.Errorf("could not parse DER encoded key: %v", err)
} }
return x509.ParsePKCS1PrivateKey(decryptedPEMBlock) tufRSAPrivateKey, err := RSAToPrivateKey(rsaPrivKey)
if err != nil {
return nil, fmt.Errorf("could not convert rsa.PrivateKey to data.PrivateKey: %v", err)
}
return tufRSAPrivateKey, nil
default: default:
return nil, fmt.Errorf("unsupported key type %q", block.Type) return nil, fmt.Errorf("unsupported key type %q", block.Type)
} }
} }
// GenerateRSAKey generates an RSA Private key and returns a TUF PrivateKey
func GenerateRSAKey(random io.Reader, bits int) (*data.PrivateKey, error) {
rsaPrivKey, err := rsa.GenerateKey(random, bits)
if err != nil {
return nil, fmt.Errorf("could not generate private key: %v", err)
}
return RSAToPrivateKey(rsaPrivKey)
}
// RSAToPrivateKey converts an rsa.Private key to a TUF data.PrivateKey type
func RSAToPrivateKey(rsaPrivKey *rsa.PrivateKey) (*data.PrivateKey, error) {
// Get a DER-encoded representation of the PublicKey
rsaPubBytes, err := x509.MarshalPKIXPublicKey(&rsaPrivKey.PublicKey)
if err != nil {
return nil, fmt.Errorf("failed to marshal private key: %v", err)
}
// Get a DER-encoded representation of the PrivateKey
rsaPrivBytes := x509.MarshalPKCS1PrivateKey(rsaPrivKey)
return data.NewPrivateKey("RSA", rsaPubBytes, rsaPrivBytes), nil
}
// NewCertificate returns an X509 Certificate following a template, given a GUN.
func NewCertificate(gun string) (*x509.Certificate, error) {
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 {
return nil, fmt.Errorf("failed to generate new certificate: %v", err)
}
// TODO(diogo): Currently hard coding organization to be the gun. Revisit.
return &x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{gun},
CommonName: gun,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning},
BasicConstraintsValid: true,
}, nil
}