diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 14ce8748df..00d93dafab 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -43,11 +43,11 @@ }, { "ImportPath": "github.com/docker/rufus/proto", - "Rev": "61b53384b24bfa83e8e0a5f11f28ae83457fd80c" + "Rev": "7f61f678c264ae0a329f25cbaa8af6fd55ada7b6" }, { "ImportPath": "github.com/endophage/gotuf", - "Rev": "682ec56d6a7b60e432bc2560e17d8e1aec84d171" + "Rev": "66da486b58ef378c96433af965f61ca0efaccb9a" }, { "ImportPath": "github.com/go-sql-driver/mysql", diff --git a/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.pb.go b/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.pb.go index 49217eb51d..9cbfb6a14f 100644 --- a/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.pb.go +++ b/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.pb.go @@ -10,6 +10,7 @@ It is generated from these files: It has these top-level messages: KeyID + Algorithm PublicKey Signature SignatureRequest @@ -40,10 +41,20 @@ func (m *KeyID) Reset() { *m = KeyID{} } func (m *KeyID) String() string { return proto1.CompactTextString(m) } 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 { - KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` - PublicKey []byte `protobuf:"bytes,2,opt,name=publicKey,proto3" json:"publicKey,omitempty"` + KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,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{} } @@ -57,10 +68,18 @@ func (m *PublicKey) GetKeyID() *KeyID { 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 { - KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` - Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,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{} } @@ -74,10 +93,18 @@ func (m *Signature) GetKeyID() *KeyID { 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 { - KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,omitempty"` - Content []byte `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + KeyID *KeyID `protobuf:"bytes,1,opt,name=keyID" json:"keyID,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{} } @@ -91,6 +118,13 @@ func (m *SignatureRequest) GetKeyID() *KeyID { return nil } +func (m *SignatureRequest) GetAlgorithm() *Algorithm { + if m != nil { + return m.Algorithm + } + return nil +} + // Void represents an empty message type type Void struct { } diff --git a/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.proto b/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.proto index 403ba16437..7d11a106ae 100644 --- a/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.proto +++ b/Godeps/_workspace/src/github.com/docker/rufus/proto/rufus.proto @@ -26,22 +26,30 @@ message KeyID { 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 { 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 { 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 { KeyID keyID = 1; - bytes content = 2; + Algorithm algorithm = 2; + bytes content = 3; } // Void represents an empty message type diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/CONTRIBUTORS b/Godeps/_workspace/src/github.com/endophage/gotuf/CONTRIBUTORS new file mode 100644 index 0000000000..5f484889cb --- /dev/null +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/CONTRIBUTORS @@ -0,0 +1,3 @@ +Aaron Lehmann (github: aaronlehmann) +Lewis Marshall (github: lmars) +Jonathan Rudenberg (github: titanous) diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/sign_test.go b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/sign_test.go index 3cfb9e5018..9c204644c5 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/sign_test.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/sign_test.go @@ -1,14 +1,22 @@ package signed import ( + "encoding/pem" "testing" "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 { - testKey keys.PublicKey + testKey data.PublicKey } 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 } -func (mts *MockCryptoService) Create() (*keys.PublicKey, error) { +func (mts *MockCryptoService) Create(_ string) (*data.PublicKey, error) { return &mts.testKey, nil } -func (mts *MockCryptoService) PublicKeys(keyIDs ...string) (map[string]*keys.PublicKey, error) { - keys := map[string]*keys.PublicKey{"testID": &mts.testKey} +func (mts *MockCryptoService) PublicKeys(keyIDs ...string) (map[string]*data.PublicKey, error) { + keys := map[string]*data.PublicKey{"testID": &mts.testKey} return keys, nil } @@ -32,8 +40,10 @@ var _ CryptoService = &MockCryptoService{} // Test signing and ensure the expected signature is added func TestBasicSign(t *testing.T) { + testKey, _ := pem.Decode([]byte(testKeyPEM1)) + k := data.NewPublicKey("rsa", testKey.Bytes) signer := Signer{&MockCryptoService{ - testKey: keys.PublicKey{ID: "testID"}, + testKey: *k, }} key, err := signer.Create("root") if err != nil { @@ -47,7 +57,7 @@ func TestBasicSign(t *testing.T) { 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) } @@ -57,20 +67,21 @@ func TestBasicSign(t *testing.T) { // 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) func TestReSign(t *testing.T) { + testKey, _ := pem.Decode([]byte(testKeyPEM1)) + k := data.NewPublicKey("rsa", testKey.Bytes) signer := Signer{&MockCryptoService{ - testKey: keys.PublicKey{}, + testKey: *k, }} - key := keys.PublicKey{ID: "testID"} testData := data.Signed{} - signer.Sign(&testData, &key) - signer.Sign(&testData, &key) + signer.Sign(&testData, k) + signer.Sign(&testData, k) if len(testData.Signatures) != 1 { 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) } @@ -78,19 +89,21 @@ func TestReSign(t *testing.T) { func TestMultiSign(t *testing.T) { signer := Signer{&MockCryptoService{}} - key := keys.PublicKey{ID: "testID1"} 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"} - signer.Sign(&testData, &key) + testKey, _ = pem.Decode([]byte(testKeyPEM2)) + key = data.NewPublicKey("rsa", testKey.Bytes) + signer.Sign(&testData, key) if len(testData.Signatures) != 2 { 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 { if _, ok := keyIDs[sig.KeyID]; !ok { 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) { + testKey, _ := pem.Decode([]byte(testKeyPEM1)) + k := data.NewPublicKey("rsa", testKey.Bytes) signer := Signer{&MockCryptoService{ - testKey: keys.PublicKey{ID: "testID"}, + testKey: *k, }} key, err := signer.Create("root") @@ -109,7 +124,7 @@ func TestCreate(t *testing.T) { if err != nil { t.Fatal(err) } - if key.ID != "testID" { - t.Fatalf("Expected key ID not found: %s", key.ID) + if key.ID() != testKeyID1 { + t.Fatalf("Expected key ID not found: %s", key.ID()) } } diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verifiers_test.go b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verifiers_test.go index b51ae0c15d..4f57d2ea6b 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verifiers_test.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verifiers_test.go @@ -1,10 +1,10 @@ package signed import ( - "crypto" - "crypto/rsa" - "crypto/sha256" - "crypto/x509" + _ "crypto" + _ "crypto/rsa" + _ "crypto/sha256" + _ "crypto/x509" "testing" ) diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verify_test.go b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verify_test.go index b2dde99cd7..f9f5748d3f 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verify_test.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/signed/verify_test.go @@ -23,7 +23,7 @@ func (VerifySuite) Test(c *C) { signer := NewSigner(trust) type test struct { name string - keys []*keys.PublicKey + keys []*data.PublicKey roles map[string]*data.Role s *data.Signed ver int @@ -81,7 +81,7 @@ func (VerifySuite) Test(c *C) { k, _ := signer.Create("root") signer.Sign(t.s, 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", exp: &expiredTime, - err: ErrExpired{expiredTime}, + err: ErrExpired{expiredTime.Format("2006-01-02 15:04:05 MST")}, }, } for _, t := range tests { @@ -154,24 +154,27 @@ func (VerifySuite) Test(c *C) { t.exp = &expires } if t.typ == "" { - t.typ = t.role + t.typ = data.TUFTypes[t.role] } if t.keys == nil && t.s == nil { 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) c.Assert(err, IsNil) s := &data.Signed{Signed: b} signer.Sign(s, k) t.s = s - t.keys = []*keys.PublicKey{k} + t.keys = []*data.PublicKey{k} } if t.roles == nil { t.roles = map[string]*data.Role{ "root": &data.Role{ - KeyIDs: []string{t.keys[0].ID}, - Threshold: 1, + RootRole: data.RootRole{ + KeyIDs: []string{t.keys[0].ID()}, + Threshold: 1, + }, + Name: "root", }, } } @@ -181,11 +184,10 @@ func (VerifySuite) Test(c *C) { db := keys.NewDB() for _, k := range t.keys { - err := db.AddKey(k) - c.Assert(err, IsNil) + db.AddKey(k) } - for n, r := range t.roles { - err := db.AddRole(n, r) + for _, r := range t.roles { + err := db.AddRole(r) c.Assert(err, IsNil) } @@ -203,5 +205,5 @@ func assertErrExpired(c *C, err error, expected ErrExpired) { if !ok { 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) } diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/store/errors.go b/Godeps/_workspace/src/github.com/endophage/gotuf/store/errors.go new file mode 100644 index 0000000000..69d2090f04 --- /dev/null +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/store/errors.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/store/httpstore.go b/Godeps/_workspace/src/github.com/endophage/gotuf/store/httpstore.go index fcc2d838fb..86299ba911 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/store/httpstore.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/store/httpstore.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "io/ioutil" + "net/http" "net/url" "path" @@ -61,6 +62,9 @@ func (s HTTPStore) GetMeta(name string, size int64) (json.RawMessage, error) { return nil, err } defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + return nil, &ErrMetaNotFound{role: name} + } b := io.LimitReader(resp.Body, int64(size)) body, err := ioutil.ReadAll(b) diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go index efca615620..cd95fc9e1e 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf.go @@ -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 // roles. 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) rootKeys := make(map[string]*data.PublicKey) for _, r := range data.ValidRoles { @@ -199,15 +215,21 @@ func (tr *TufRepo) InitRepo(consistent bool) error { return err } tr.Root = root + return nil +} +func (tr *TufRepo) InitTargets() error { targets := data.NewTargets() 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 { return err } - signedTargets, err := tr.SignTargets("targets", data.DefaultExpires("targets")) + signedTargets, err := tr.SignTargets("targets", data.DefaultExpires("targets"), nil) if err != nil { return err } @@ -216,8 +238,11 @@ func (tr *TufRepo) InitRepo(consistent bool) error { return err } 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 { return err } @@ -428,7 +453,7 @@ func (tr *TufRepo) UpdateTimestamp(s *data.Signed) error { 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") if tr.Root.Dirty { tr.Root.Signed.Version++ @@ -438,7 +463,7 @@ func (tr *TufRepo) SignRoot(expires time.Time) (*data.Signed, error) { if err != nil { return nil, err } - signed, err = tr.sign(signed, *root) + signed, err = tr.sign(signed, *root, signer) if err != nil { return nil, err } @@ -446,7 +471,7 @@ func (tr *TufRepo) SignRoot(expires time.Time) (*data.Signed, error) { 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("Got targets data.Signed object") 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) logrus.Debug("About to sign ", role) - signed, err = tr.sign(signed, *targets) + signed, err = tr.sign(signed, *targets, signer) if err != nil { logrus.Debug("errored signing ", role) 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") if tr.Root.Dirty { - signedRoot, err := tr.SignRoot(data.DefaultExpires("root")) + signedRoot, err := tr.SignRoot(data.DefaultExpires("root"), signer) if err != nil { return nil, err } @@ -493,7 +518,7 @@ func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) { if !targets.Dirty { continue } - signedTargets, err := tr.SignTargets(role, data.DefaultExpires("targets")) + signedTargets, err := tr.SignTargets(role, data.DefaultExpires("targets"), signer) if err != nil { return nil, err } @@ -510,7 +535,7 @@ func (tr *TufRepo) SignSnapshot(expires time.Time) (*data.Signed, error) { return nil, err } snapshot := tr.keysDB.GetRole(data.ValidRoles["snapshot"]) - signed, err = tr.sign(signed, *snapshot) + signed, err = tr.sign(signed, *snapshot, signer) if err != nil { 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") if tr.Snapshot.Dirty { - signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot")) + signedSnapshot, err := tr.SignSnapshot(data.DefaultExpires("snapshot"), signer) if err != nil { return nil, err } @@ -544,7 +569,7 @@ func (tr *TufRepo) SignTimestamp(expires time.Time) (*data.Signed, error) { return nil, err } timestamp := tr.keysDB.GetRole(data.ValidRoles["timestamp"]) - signed, err = tr.sign(signed, *timestamp) + signed, err = tr.sign(signed, *timestamp, signer) if err != nil { 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)) for _, kid := range role.KeyIDs { 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 { return nil, keys.ErrInvalidKey } - err := tr.signer.Sign(signed, ks...) - if err != nil { - return nil, err + if signer != nil { + err := signer.Sign(signed, ks...) + if err != nil { + return nil, err + } + } else { + err := tr.signer.Sign(signed, ks...) + if err != nil { + return nil, err + } } return signed, nil } diff --git a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf_test.go b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf_test.go index 01722ae91f..21a895165a 100644 --- a/Godeps/_workspace/src/github.com/endophage/gotuf/tuf_test.go +++ b/Godeps/_workspace/src/github.com/endophage/gotuf/tuf_test.go @@ -88,7 +88,7 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) { if err != nil { t.Fatal(err) } - signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) + signedRoot, err := repo.SignRoot(data.DefaultExpires("root"), nil) if err != nil { t.Fatal(err) } @@ -96,7 +96,7 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) { ioutil.WriteFile(dir+"/root.json", rootJSON, 0755) 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 { t.Fatal(err) } @@ -107,14 +107,14 @@ func writeRepo(t *testing.T, dir string, repo *TufRepo) { ioutil.WriteFile(p, targetsJSON, 0755) } - signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot")) + signedSnapshot, err := repo.SignSnapshot(data.DefaultExpires("snapshot"), nil) if err != nil { t.Fatal(err) } snapshotJSON, _ := json.Marshal(signedSnapshot) 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 { t.Fatal(err) } diff --git a/client/changelist/change.go b/client/changelist/change.go new file mode 100644 index 0000000000..77544dc661 --- /dev/null +++ b/client/changelist/change.go @@ -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 +} diff --git a/client/changelist/changelist.go b/client/changelist/changelist.go new file mode 100644 index 0000000000..e6b16df855 --- /dev/null +++ b/client/changelist/changelist.go @@ -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 +} diff --git a/client/changelist/changelist_test.go b/client/changelist/changelist_test.go new file mode 100644 index 0000000000..664cf17e16 --- /dev/null +++ b/client/changelist/changelist_test.go @@ -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") +} diff --git a/client/changelist/files_changelist.go b/client/changelist/files_changelist.go new file mode 100644 index 0000000000..42f201b24e --- /dev/null +++ b/client/changelist/files_changelist.go @@ -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 +} diff --git a/client/changelist/files_changelist_test.go b/client/changelist/files_changelist_test.go new file mode 100644 index 0000000000..848e3bb785 --- /dev/null +++ b/client/changelist/files_changelist_test.go @@ -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") +} diff --git a/client/changelist/interface.go b/client/changelist/interface.go new file mode 100644 index 0000000000..fd24b65c54 --- /dev/null +++ b/client/changelist/interface.go @@ -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 +} diff --git a/client/cli_crypto_service.go b/client/cli_crypto_service.go new file mode 100644 index 0000000000..3a6c9839e6 --- /dev/null +++ b/client/cli_crypto_service.go @@ -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 +} diff --git a/client/client.go b/client/client.go new file mode 100644 index 0000000000..80299960fc --- /dev/null +++ b/client/client.go @@ -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 +} diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 0000000000..0b541e6643 --- /dev/null +++ b/client/client_test.go @@ -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") + } + } +} diff --git a/client/helpers.go b/client/helpers.go new file mode 100644 index 0000000000..a92f1e711b --- /dev/null +++ b/client/helpers.go @@ -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) +} diff --git a/cmd/notary-server/config.json b/cmd/notary-server/config.json index 830334db7a..2af3d7c221 100644 --- a/cmd/notary-server/config.json +++ b/cmd/notary-server/config.json @@ -4,10 +4,11 @@ "tls_cert_file": "./fixtures/notary.pem", "tls_key_file": "./fixtures/notary.key" }, - "trust_service":{ + "trust_service": { "type": "local", - "hostname": "", - "port": "" + "hostname": "rufus", + "port": "7899", + "tls_ca_file": "./fixtures/ca.cert" }, "logging": { "level": 5 diff --git a/cmd/notary/cli_crypto_service.go b/cmd/notary/cli_crypto_service.go deleted file mode 100644 index 3387e619b2..0000000000 --- a/cmd/notary/cli_crypto_service.go +++ /dev/null @@ -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) -} diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index f8ec61bcf7..7b3cc58923 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -1,12 +1,9 @@ package main import ( - "crypto/rand" "crypto/x509" - "crypto/x509/pkix" "fmt" "math" - "math/big" "net/url" "os" "path/filepath" @@ -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. - keyList := privKeyStore.ListDir(gunOrID) + keyList := privKeyStore.ListDir(gunOrID, true) if len(keyList) < 1 { 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("# Signing keys: ") - for _, k := range privKeyStore.ListAll() { + for _, k := range privKeyStore.ListFiles(true) { printKey(k) } } @@ -207,39 +204,14 @@ func keysGenerate(cmd *cobra.Command, args []string) { fatalf("invalid Global Unique Name: %s", gun) } - _, cert, err := generateKeyAndCert(gun) - if err != nil { - fatalf("could not generate key: %v", err) - } + // _, cert, err := generateKeyAndCert(gun) + // if err != nil { + // fatalf("could not generate key: %v", err) + // } - certificateStore.AddCert(cert) - fingerprint := trustmanager.FingerprintCert(cert) - fmt.Println("Generated new keypair with ID: ", fingerprint) -} - -func newCertificate(gun, organization string) *x509.Certificate { - notBefore := time.Now() - notAfter := notBefore.Add(time.Hour * 24 * 365 * 2) - - serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) - serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) - if err != nil { - fatalf("failed to generate serial number: %s", err) - } - - return &x509.Certificate{ - SerialNumber: serialNumber, - Subject: pkix.Name{ - Organization: []string{organization}, - CommonName: gun, - }, - NotBefore: notBefore, - NotAfter: notAfter, - - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - BasicConstraintsValid: true, - } + // certificateStore.AddCert(cert) + // fingerprint := trustmanager.FingerprintCert(cert) + // fmt.Println("Generated new keypair with ID: ", fingerprint) } func printCert(cert *x509.Certificate) { diff --git a/cmd/notary/main.go b/cmd/notary/main.go index a8bf9b61a1..174401d24d 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -16,18 +16,15 @@ import ( ) const configFileName string = "config" - -// Default paths should end with a '/' so directory creation works correctly const configPath string = ".docker/trust/" -const trustDir string = configPath + "trusted_certificates/" -const privDir string = configPath + "private/" -const tufDir string = configPath + "tuf/" - -var caStore trustmanager.X509Store -var certificateStore trustmanager.X509Store -var privKeyStore trustmanager.EncryptedFileStore +const trustDir string = "trusted_certificates/" +const privDir string = "private/" +const rootKeysDir string = "root_keys/" var rawOutput bool +var caStore trustmanager.X509Store +var certificateStore trustmanager.X509Store +var privKeyStore trustmanager.FileStore func init() { logrus.SetLevel(logrus.DebugLevel) @@ -59,13 +56,11 @@ func init() { } // Set up the defaults for our config - viper.SetDefault("trustDir", path.Join(homeDir, path.Dir(trustDir))) - viper.SetDefault("privDir", path.Join(homeDir, path.Dir(privDir))) - viper.SetDefault("tufDir", path.Join(homeDir, path.Dir(tufDir))) + viper.SetDefault("baseTrustDir", path.Join(homeDir, path.Dir(configPath))) // Get the final value for the CA directory - finalTrustDir := viper.GetString("trustDir") - finalPrivDir := viper.GetString("privDir") + finalTrustDir := path.Join(viper.GetString("baseTrustDir"), trustDir) + finalPrivDir := path.Join(viper.GetString("baseTrustDir"), privDir) // Load all CAs that aren't expired and don't use SHA1 caStore, err = trustmanager.NewX509FilteredFileStore(finalTrustDir, func(cert *x509.Certificate) bool { @@ -76,10 +71,10 @@ func init() { cert.SignatureAlgorithm != x509.ECDSAWithSHA1 }) 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 { return !cert.IsCA && time.Now().Before(cert.NotAfter) && @@ -88,12 +83,12 @@ func init() { cert.SignatureAlgorithm != x509.ECDSAWithSHA1 }) if err != nil { - fatalf("could not create X509FileStore: %v", err) + fatalf("could not create Certificate X509FileStore: %v", err) } privKeyStore, err = trustmanager.NewKeyFileStore(finalPrivDir) 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(cmdTufInit) NotaryCmd.AddCommand(cmdTufList) - cmdTufList.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary list to output a non-pretty printed version of the targets list. Useful if you need to parse the list.") + cmdTufList.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary list to output a nonpretty printed version of the targets list. Useful if you need to parse the list.") NotaryCmd.AddCommand(cmdTufAdd) NotaryCmd.AddCommand(cmdTufRemove) NotaryCmd.AddCommand(cmdTufPublish) cmdTufPublish.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location") NotaryCmd.AddCommand(cmdTufLookup) - cmdTufLookup.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary lookup to output a non-pretty printed version of the targets list. Useful if you need to parse the list.") + cmdTufLookup.Flags().BoolVarP(&rawOutput, "raw", "", false, "Instructs notary lookup to output a nonpretty printed version of the targets list. Useful if you need to parse the list.") cmdTufLookup.Flags().StringVarP(&remoteTrustServer, "remote", "r", "", "Remote trust server location") NotaryCmd.AddCommand(cmdVerify) diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 958a0d68ae..46b12affd0 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -1,29 +1,21 @@ package main import ( - "bytes" "crypto/sha256" - "crypto/x509" - "encoding/json" - "encoding/pem" + "errors" "fmt" "io/ioutil" "os" - "path" - "path/filepath" "github.com/Sirupsen/logrus" - "github.com/docker/notary/trustmanager" - "github.com/endophage/gotuf" - "github.com/endophage/gotuf/client" - "github.com/endophage/gotuf/data" - "github.com/endophage/gotuf/keys" - "github.com/endophage/gotuf/signed" - "github.com/endophage/gotuf/store" + notaryclient "github.com/docker/notary/client" "github.com/spf13/cobra" "github.com/spf13/viper" ) +// FIXME: This should not be hardcoded +const hardcodedBaseURL = "https://notary:4443" + var remoteTrustServer string var cmdTufList = &cobra.Command{ @@ -84,30 +76,21 @@ func tufAdd(cmd *cobra.Command, args []string) { gun := args[0] targetName := args[1] targetPath := args[2] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - repo := tuf.NewTufRepo(kdb, signer) - b, err := ioutil.ReadFile(targetPath) + repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { fatalf(err.Error()) } - filestore := bootstrapRepo(gun, repo) - - fmt.Println("Generating metadata for target") - meta, err := data.NewFileMeta(bytes.NewBuffer(b)) + target, err := notaryclient.NewTarget(targetName, targetPath) if err != nil { fatalf(err.Error()) } - - fmt.Printf("Adding target \"%s\" with sha256 \"%s\" and size %d bytes.\n", targetName, meta.Hashes["sha256"], meta.Length) - _, err = repo.AddTargets("targets", data.Files{targetName: meta}) + err = repo.AddTarget(target) if err != nil { fatalf(err.Error()) } - - saveRepo(repo, filestore) + fmt.Println("Successfully added targets") } func tufInit(cmd *cobra.Command, args []string) { @@ -117,91 +100,44 @@ func tufInit(cmd *cobra.Command, args []string) { } gun := args[0] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - remote, err := getRemoteStore(gun) - rawTSKey, err := remote.GetKey("timestamp") - if err != nil { - fatalf(err.Error()) - } - fmt.Println("RawKey: ", string(rawTSKey)) - parsedKey := &data.TUFKey{} - err = json.Unmarshal(rawTSKey, parsedKey) - if err != nil { - fatalf(err.Error()) - } - timestampKey := data.NewPublicKey(parsedKey.Cipher(), parsedKey.Public()) - - rootKey, err := signer.Create("root") - if err != nil { - fatalf(err.Error()) - } - targetsKey, err := signer.Create("targets") - if err != nil { - fatalf(err.Error()) - } - snapshotKey, err := signer.Create("snapshot") + nRepo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { fatalf(err.Error()) } - kdb.AddKey(rootKey) - kdb.AddKey(targetsKey) - kdb.AddKey(snapshotKey) - kdb.AddKey(timestampKey) + 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()) + } + } - rootRole, err := data.NewRole("root", 1, []string{rootKey.ID()}, nil, nil) - 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) + rootSigner, err := nRepo.GetRootSigner(rootKeyID, passphrase) if err != nil { fatalf(err.Error()) } - err = kdb.AddRole(rootRole) + nRepo.Initialize(rootSigner) if err != nil { fatalf(err.Error()) } - err = kdb.AddRole(targetsRole) - if err != nil { - fatalf(err.Error()) - } - err = kdb.AddRole(snapshotRole) - if err != nil { - fatalf(err.Error()) - } - err = kdb.AddRole(timestampRole) - if err != nil { - fatalf(err.Error()) - } - - repo := tuf.NewTufRepo(kdb, signer) - - filestore, err := store.NewFilesystemStore( - path.Join(viper.GetString("tufDir"), gun), // TODO: base trust dir from config - "metadata", - "json", - "targets", - ) - if err != nil { - fatalf(err.Error()) - } - - err = repo.InitRepo(false) - if err != nil { - fatalf(err.Error()) - } - saveRepo(repo, filestore) } func tufList(cmd *cobra.Command, args []string) { @@ -210,31 +146,21 @@ func tufList(cmd *cobra.Command, args []string) { fatalf("must specify a GUN") } gun := args[0] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - remote, err := getRemoteStore(gun) + repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { - return - } - c, err := bootstrapClient(gun, remote, repo, kdb) - if err != nil { - return - } - err = c.Update() - if err != nil { - logrus.Error("Error updating client: ", err.Error()) - return + fatalf(err.Error()) } - if rawOutput { - for name, meta := range repo.Targets["targets"].Signed.Targets { - fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) - } - } else { - for name, meta := range repo.Targets["targets"].Signed.Targets { - fmt.Println(name, " ", meta.Hashes["sha256"], " ", meta.Length) - } + // Retreive the remote list of signed targets + targetList, err := repo.ListTargets() + if err != nil { + fatalf(err.Error()) + } + + // 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] targetName := args[1] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - remote, err := getRemoteStore(gun) - c, err := bootstrapClient(gun, remote, repo, kdb) + repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { - return + fatalf(err.Error()) } - err = c.Update() + + // TODO(diogo): Parse Targets and print them + target, err := repo.GetTargetByName(targetName) if err != nil { - logrus.Error("Error updating client: ", err.Error()) - return - } - meta := c.TargetMeta(targetName) - if meta == nil { - logrus.Infof("Target %s not found in %s.", targetName, gun) - return - } - if rawOutput { - fmt.Println(targetName, fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]), meta.Length) - } else { - fmt.Println(targetName, fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]), meta.Length) + fatalf(err.Error()) } + + fmt.Println(target.Name, fmt.Sprintf("sha256:%s", target.Hashes["sha256"]), target.Length) } func tufPublish(cmd *cobra.Command, args []string) { @@ -277,41 +193,15 @@ func tufPublish(cmd *cobra.Command, args []string) { } gun := args[0] + fmt.Println("Pushing changes to ", gun, ".") - remote, err := getRemoteStore(gun) - filestore, err := store.NewFilesystemStore( - path.Join(viper.GetString("tufDir"), gun), - "metadata", - "json", - "targets", - ) + repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { fatalf(err.Error()) } - root, err := filestore.GetMeta("root", 0) - if err != nil { - fatalf(err.Error()) - } - targets, err := filestore.GetMeta("targets", 0) - if err != nil { - fatalf(err.Error()) - } - snapshot, err := filestore.GetMeta("snapshot", 0) - if err != nil { - fatalf(err.Error()) - } - - err = remote.SetMeta("root", root) - if err != nil { - fatalf(err.Error()) - } - err = remote.SetMeta("targets", targets) - if err != nil { - fatalf(err.Error()) - } - err = remote.SetMeta("snapshot", snapshot) + err = repo.Publish(passphraseRetriever) if err != nil { fatalf(err.Error()) } @@ -324,20 +214,15 @@ func tufRemove(cmd *cobra.Command, args []string) { } gun := args[0] targetName := args[1] - kdb := keys.NewDB() - signer := signed.NewSigner(NewCryptoService(gun)) - repo := tuf.NewTufRepo(kdb, signer) + //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) - - 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) { @@ -356,31 +241,21 @@ func verify(cmd *cobra.Command, args []string) { //TODO (diogo): This code is copy/pasted from lookup. gun := args[0] targetName := args[1] - kdb := keys.NewDB() - repo := tuf.NewTufRepo(kdb, nil) - - remote, err := getRemoteStore(gun) - - c, err := bootstrapClient(gun, remote, repo, kdb) + repo, err := notaryclient.NewNotaryRepository(viper.GetString("baseTrustDir"), gun, hardcodedBaseURL) if err != nil { - logrus.Error("Unable to setup client.") - return - } - - err = c.Update() - if err != nil { - fmt.Println("Update failed") fatalf(err.Error()) } - meta := c.TargetMeta(targetName) - if meta == nil { + + // TODO(diogo): Parse Targets and print them + target, err := repo.GetTargetByName(targetName) + if err != nil { logrus.Error("notary: data not present in the trusted collection.") - os.Exit(1) + os.Exit(-11) } // Create hasher and hash data stdinHash := fmt.Sprintf("sha256:%x", sha256.Sum256(payload)) - serverHash := fmt.Sprintf("sha256:%s", meta.Hashes["sha256"]) + serverHash := fmt.Sprintf("sha256:%s", target.Hashes["sha256"]) if stdinHash != serverHash { logrus.Error("notary: data not present in the trusted collection.") os.Exit(1) @@ -390,179 +265,16 @@ func verify(cmd *cobra.Command, args []string) { return } -func saveRepo(repo *tuf.TufRepo, filestore store.MetadataStore) error { - fmt.Println("Saving changes to Trusted Collection.") - signedRoot, err := repo.SignRoot(data.DefaultExpires("root")) +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 } - rootJSON, _ := json.Marshal(signedRoot) - filestore.SetMeta("root", rootJSON) - - for r, _ := range repo.Targets { - signedTargets, err := repo.SignTargets(r, data.DefaultExpires("targets")) - if err != nil { - return err - } - targetsJSON, _ := json.Marshal(signedTargets) - parentDir := filepath.Dir(r) - os.MkdirAll(parentDir, 0755) - filestore.SetMeta(r, targetsJSON) + 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") } - - 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", - ) + return passphrase, nil } diff --git a/q b/q new file mode 100644 index 0000000000..c5a8699982 --- /dev/null +++ b/q @@ -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 + } diff --git a/server/handlers/default.go b/server/handlers/default.go index da594abe7c..3e0cf1572f 100644 --- a/server/handlers/default.go +++ b/server/handlers/default.go @@ -35,7 +35,7 @@ func MainHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *e 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 { defer r.Body.Close() s := ctx.Value("metaStore") @@ -102,6 +102,13 @@ func GetHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *er tufRole := vars["tufRole"] out, err := store.GetCurrent(gun, tufRole) 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) return &errors.HTTPError{ HTTPStatus: http.StatusInternalServerError, @@ -147,6 +154,7 @@ func DeleteHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) return nil } +// GetTimestampHandler returns a timestamp.json given a GUN func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Request) *errors.HTTPError { s := ctx.Value("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) if err != nil { + if _, ok := err.(*storage.ErrNoKey); ok { + return &errors.HTTPError{ + HTTPStatus: http.StatusNotFound, + Code: 9999, + Err: err, + } + } return &errors.HTTPError{ HTTPStatus: http.StatusInternalServerError, Code: 9999, @@ -184,6 +199,8 @@ func GetTimestampHandler(ctx context.Context, w http.ResponseWriter, r *http.Req 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 { s := ctx.Value("metaStore") store, ok := s.(storage.MetaStore) diff --git a/server/server.go b/server/server.go index dd42a28b90..d65262e8d2 100644 --- a/server/server.go +++ b/server/server.go @@ -58,7 +58,7 @@ func Run(ctx context.Context, addr, tlsCertFile, tlsKeyFile string, trust signed lsnr = tls.NewListener(lsnr, tlsConfig) } - var ac auth.AccessController = nil + var ac auth.AccessController hand := utils.RootHandlerFactory(ac, ctx, trust) r := mux.NewRouter() diff --git a/server/storage/database.go b/server/storage/database.go index d23ea278e1..c3cdd554d6 100644 --- a/server/storage/database.go +++ b/server/storage/database.go @@ -27,13 +27,14 @@ type MySQLStorage struct { sql.DB } +// NewMySQLStorage is a convenience method to create a MySQLStorage func NewMySQLStorage(db *sql.DB) *MySQLStorage { return &MySQLStorage{ 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 func (db *MySQLStorage) UpdateCurrent(gun, role string, version int, data []byte) error { 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 } -// Get a specific TUF record +// GetCurrent gets a specific TUF record 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;" 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 } +// Delete deletes all the records for a specific GUN func (db *MySQLStorage) Delete(gun string) error { stmt := "DELETE FROM `tuf_files` WHERE `gun`=?;" _, err := db.Exec(stmt, gun) return err } + +// GetTimestampKey returns the timestamps Public Key data func (db *MySQLStorage) GetTimestampKey(gun string) (cipher string, public []byte, err error) { stmt := "SELECT `cipher`, `public` FROM `timestamp_keys` WHERE `gun`=?;" row := db.QueryRow(stmt, gun) @@ -100,6 +104,8 @@ func (db *MySQLStorage) GetTimestampKey(gun string) (cipher string, public []byt 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 { stmt := "INSERT INTO `timestamp_keys` (`gun`, `cipher`, `public`) VALUES (?,?,?);" _, err := db.Exec(stmt, gun, cipher, public) diff --git a/server/storage/errors.go b/server/storage/errors.go index 14be8ab02b..f2d58c3a3e 100644 --- a/server/storage/errors.go +++ b/server/storage/errors.go @@ -4,30 +4,38 @@ import ( "fmt" ) +// ErrOldVersion is returned when a newer version of TUF metadada is already available type ErrOldVersion struct{} +// ErrOldVersion is returned when a newer version of TUF metadada is already available func (err ErrOldVersion) Error() string { 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{} +// Error implements error func (err ErrNotFound) Error() string { return fmt.Sprintf("No record found") } +// ErrTimestampKeyExists is returned when a timestamp key already exists type ErrTimestampKeyExists struct { gun string } +// ErrTimestampKeyExists is returned when a timestamp key already exists func (err ErrTimestampKeyExists) Error() string { return fmt.Sprintf("Error, timestamp key already exists for %s", err.gun) } +// ErrNoKey is returned when no timestamp key is found type ErrNoKey struct { gun string } +// ErrNoKey is returned when no timestamp key is found func (err ErrNoKey) Error() string { return fmt.Sprintf("Error, no timestamp key found for %s", err.gun) } diff --git a/server/storage/interface.go b/server/storage/interface.go index 2fefbdafe0..25ee4f644e 100644 --- a/server/storage/interface.go +++ b/server/storage/interface.go @@ -1,5 +1,6 @@ package storage +// MetaStore holds the methods that are used for a Metadata Store type MetaStore interface { UpdateCurrent(gun, role string, version int, data []byte) error GetCurrent(gun, tufRole string) (data []byte, err error) diff --git a/server/storage/memory.go b/server/storage/memory.go index 6d3233437c..a4d4bfe441 100644 --- a/server/storage/memory.go +++ b/server/storage/memory.go @@ -16,23 +16,24 @@ type ver struct { 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 -type memStorage struct { +type MemStorage struct { lock sync.Mutex tufMeta map[string][]*ver tsKeys map[string]*key } // NewMemStorage instantiates a memStorage instance -func NewMemStorage() *memStorage { - return &memStorage{ +func NewMemStorage() *MemStorage { + return &MemStorage{ tufMeta: make(map[string][]*ver), 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) st.lock.Lock() defer st.lock.Unlock() @@ -47,7 +48,8 @@ func (st *memStorage) UpdateCurrent(gun, role string, version int, data []byte) 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) st.lock.Lock() 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 } -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() defer st.lock.Unlock() - for k, _ := range st.tufMeta { + for k := range st.tufMeta { if strings.HasPrefix(k, gun) { delete(st.tufMeta, k) } @@ -69,17 +72,20 @@ func (st *memStorage) Delete(gun string) error { 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 // wasn't observed - if k, ok := st.tsKeys[gun]; !ok { + k, ok := st.tsKeys[gun] + if !ok { 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} st.lock.Lock() defer st.lock.Unlock() diff --git a/signer/rufus_trust.go b/signer/rufus_trust.go index 24438d55b8..adf90b9bba 100644 --- a/signer/rufus_trust.go +++ b/signer/rufus_trust.go @@ -17,6 +17,7 @@ type RufusSigner struct { sClient pb.SignerClient } +// NewRufusSigner is a convinience method that returns RufusSigner func NewRufusSigner(hostname string, port string, tlscafile string) *RufusSigner { var opts []grpc.DialOption netAddr := net.JoinHostPort(hostname, port) @@ -53,7 +54,7 @@ func (trust *RufusSigner) Sign(keyIDs []string, toSign []byte) ([]data.Signature } signatures = append(signatures, data.Signature{ KeyID: sig.KeyID.ID, - Method: sig.Algorithm, + Method: sig.Algorithm.Algorithm, Signature: sig.Content, }) } @@ -67,7 +68,7 @@ func (trust *RufusSigner) Create(role string) (*data.PublicKey, error) { return nil, err } //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 } @@ -81,7 +82,7 @@ func (trust *RufusSigner) PublicKeys(keyIDs ...string) (map[string]*data.PublicK return nil, err } publicKeys[public.KeyID.ID] = - data.NewPublicKey(public.Algorithm, public.PublicKey) + data.NewPublicKey(public.Algorithm.Algorithm, public.PublicKey) } return publicKeys, nil } diff --git a/trustmanager/filestore.go b/trustmanager/filestore.go index af22ae1ea6..3701ebece6 100644 --- a/trustmanager/filestore.go +++ b/trustmanager/filestore.go @@ -17,14 +17,9 @@ type FileStore interface { RemoveDir(directoryName string) error Get(fileName string) ([]byte, error) GetPath(fileName string) string - ListAll() []string - ListDir(directoryName string) []string -} - -type EncryptedFileStore interface { - FileStore - AddEncrypted(fileName string, keyBytes []byte, passphrase string) error - GetDecrypted(fileName string, passphrase string) ([]byte, error) + ListFiles(symlinks bool) []string + ListDir(directoryName string, symlinks bool) []string + Link(src, dst string) error } // SimpleFileStore implements FileStore @@ -34,8 +29,8 @@ type SimpleFileStore struct { perms os.FileMode } -// NewFileStore creates a directory with 755 permissions -func NewFileStore(baseDir string, fileExt string) (FileStore, error) { +// NewSimpleFileStore creates a directory with 755 permissions +func NewSimpleFileStore(baseDir string, fileExt string) (FileStore, error) { if err := CreateDirectory(baseDir); err != nil { return nil, err } @@ -47,8 +42,8 @@ func NewFileStore(baseDir string, fileExt string) (FileStore, error) { }, nil } -// NewPrivateFileStore creates a directory with 700 permissions -func NewPrivateFileStore(baseDir string, fileExt string) (FileStore, error) { +// NewPrivateSimpleFileStore creates a directory with 700 permissions +func NewPrivateSimpleFileStore(baseDir string, fileExt string) (FileStore, error) { if err := CreatePrivateDirectory(baseDir); err != nil { return nil, err } @@ -108,19 +103,19 @@ func (f *SimpleFileStore) GetPath(name string) string { return f.genFilePath(name) } -// List lists all the files inside of a store -func (f *SimpleFileStore) ListAll() []string { - return f.list(f.baseDir) +// ListFiles lists all the files inside of a store +func (f *SimpleFileStore) ListFiles(symlinks bool) []string { + return f.list(f.baseDir, symlinks) } -// List lists all the files inside of a directory identified by a name -func (f *SimpleFileStore) ListDir(name string) []string { +// ListDir lists all the files inside of a directory identified by a name +func (f *SimpleFileStore) ListDir(name string, symlinks bool) []string { 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 -func (f *SimpleFileStore) list(path string) []string { +// list lists all the files in a directory given a full path. Ignores symlinks. +func (f *SimpleFileStore) list(path string, symlinks bool) []string { files := make([]string, 0, 0) filepath.Walk(path, func(fp string, fi os.FileInfo, err error) error { // If there are errors, ignore this particular file @@ -131,6 +126,12 @@ func (f *SimpleFileStore) list(path string) []string { if fi.IsDir() { 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) 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 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) } +// 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 func CreateDirectory(dir string) error { return createDirectory(dir, visible) diff --git a/trustmanager/filestore_test.go b/trustmanager/filestore_test.go index 2acba47fbc..392e8835b4 100644 --- a/trustmanager/filestore_test.go +++ b/trustmanager/filestore_test.go @@ -129,7 +129,7 @@ func TestRemoveDir(t *testing.T) { } } -func TestListAll(t *testing.T) { +func TestListFiles(t *testing.T) { testName := "docker.com/notary/certificate" testExt := "crt" perms := os.FileMode(0755) @@ -144,11 +144,18 @@ func TestListAll(t *testing.T) { // Create 10 randomfiles for i := 1; i <= 10; i++ { // 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) if err != nil { 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 @@ -158,11 +165,17 @@ func TestListAll(t *testing.T) { perms: perms, } - // Call the List function - files := store.ListAll() + // Call the List function. Expect 10 real files when not listing symlinks + files := store.ListFiles(false) if len(files) != 10 { 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) { @@ -196,19 +209,64 @@ func TestListDir(t *testing.T) { } // Call the ListDir function - files := store.ListDir("docker.com/") + files := store.ListDir("docker.com/", true) if len(files) != 10 { 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 { 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 { 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) { testExt := "crt" perms := os.FileMode(0755) diff --git a/trustmanager/keyfilestore.go b/trustmanager/keyfilestore.go index 43a1c1ff42..f076c794d1 100644 --- a/trustmanager/keyfilestore.go +++ b/trustmanager/keyfilestore.go @@ -1,6 +1,11 @@ package trustmanager -import "errors" +import ( + "path/filepath" + "strings" + + "github.com/endophage/gotuf/data" +) const ( keyExtension = "key" @@ -14,7 +19,7 @@ type KeyFileStore struct { // NewKeyFileStore returns a new KeyFileStore creating a private directory to // hold the keys. func NewKeyFileStore(baseDir string) (*KeyFileStore, error) { - fileStore, err := NewFileStore(baseDir, keyExtension) + fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension) if err != nil { return nil, err } @@ -22,35 +27,67 @@ func NewKeyFileStore(baseDir string) (*KeyFileStore, error) { return &KeyFileStore{fileStore}, nil } -// AddEncrypted stores the contents of a PEM-encoded private key as an encrypted PEM block -func (s *KeyFileStore) AddEncrypted(fileName string, pemKey []byte, passphrase string) error { - - privKey, err := ParsePEMPrivateKey(pemKey) +// AddKey stores the contents of a PEM-encoded private key as a PEM block +func (s *KeyFileStore) AddKey(name string, privKey *data.PrivateKey) error { + pemPrivKey, err := KeyToPEM(privKey) if err != nil { return err } - encryptedKey, err := EncryptPrivateKey(privKey, passphrase) - if err != nil { - return err - } - - return s.Add(fileName, encryptedKey) + return s.Add(name, pemPrivKey) } -// GetDecrypted decrypts and returns the PEM Encoded private key given a flename -// and a passphrase -func (s *KeyFileStore) GetDecrypted(fileName string, passphrase string) ([]byte, error) { - 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) +// GetKey returns the PrivateKey given a KeyID +func (s *KeyFileStore) GetKey(name string) (*data.PrivateKey, error) { + keyBytes, err := s.Get(name) if err != nil { 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 } diff --git a/trustmanager/keyfilestore_test.go b/trustmanager/keyfilestore_test.go index f76283d455..3cbff4ef90 100644 --- a/trustmanager/keyfilestore_test.go +++ b/trustmanager/keyfilestore_test.go @@ -3,7 +3,6 @@ package trustmanager import ( "bytes" "crypto/rand" - "crypto/rsa" "io/ioutil" "os" "path/filepath" @@ -30,19 +29,13 @@ func TestAddKey(t *testing.T) { 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 { t.Fatalf("could not generate private key: %v", err) } - // Get the PEM for the key - pemKey, err := KeyToPEM(key) - if err != nil { - t.Fatalf("failed to convert private key to PEM: %v", err) - } - - // Call the Add function - err = store.Add(testName, pemKey) + // Call the AddKey function + err = store.AddKey(testName, privKey) if err != nil { 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) } - // Call the Get function - pemKey, err := store.Get(testName) + // Call the GetKey function + privKey, err := store.GetKey(testName) if err != nil { 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) } } func TestAddEncryptedAndGetDecrypted(t *testing.T) { - testName := "docker.com/notary/root" testExt := "key" // 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) } - // Since we're generating this manually we need to add the extension '.' - expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt) - // Create our FileStore store, err := NewKeyFileStore(tempBaseDir) if err != nil { @@ -142,35 +136,38 @@ func TestAddEncryptedAndGetDecrypted(t *testing.T) { } // Generate new PrivateKey - key, err := rsa.GenerateKey(rand.Reader, 1024) + privKey, err := GenerateRSAKey(rand.Reader, 512) if err != nil { t.Fatalf("could not generate private key: %v", err) } - // Get PEM encodedd key - pemKey, err := KeyToPEM(key) - if err != nil { - t.Fatalf("Could not encode key to PEM: %v", err) - } - - // Call the Add function - err = store.AddEncrypted(testName, pemKey, "diogomonica") + // Call the AddEncryptedKey function + err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica") if err != nil { 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 { t.Fatalf("could not decrypt private key: %v", err) } - if !strings.Contains(string(pemKey), string(pemPrivKey)) { - t.Fatalf("expected private key content in the file: %s", expectedFilePath) + if !bytes.Equal(privKey.Private(), readPrivKey.Private()) { + t.Fatalf("written key and loaded key do not match") } } func TestGetDecryptedWithTamperedCipherText(t *testing.T) { - testName := "docker.com/notary/root" testExt := "key" // 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) } - // Since we're generating this manually we need to add the extension '.' - expectedFilePath := filepath.Join(tempBaseDir, testName+"."+testExt) - // Create our FileStore store, err := NewKeyFileStore(tempBaseDir) if err != nil { @@ -189,24 +183,22 @@ func TestGetDecryptedWithTamperedCipherText(t *testing.T) { } // Generate a new Private Key - key, err := rsa.GenerateKey(rand.Reader, 1024) + privKey, err := GenerateRSAKey(rand.Reader, 512) if err != nil { t.Fatalf("could not generate private key: %v", err) } - // Get PEM encodedd key - pemKey, err := KeyToPEM(key) - if err != nil { - t.Fatalf("Could not encode key to PEM: %v", err) - } - // Call the Add function - err = store.AddEncrypted(testName, pemKey, "diogomonica") + // Call the AddEncryptedKey function + err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica") if err != nil { 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 - fp, _ := os.OpenFile(expectedFilePath, os.O_WRONLY, 0600) + fp, err := os.OpenFile(expectedFilePath, os.O_WRONLY, 0600) if err != nil { t.Fatalf("expected file not found: %v", err) } @@ -215,7 +207,7 @@ func TestGetDecryptedWithTamperedCipherText(t *testing.T) { fp.WriteAt([]byte("a"), int64(1)) // Try to decrypt the file - _, err = store.GetDecrypted(testName, "diogomonica") + _, err = store.GetDecryptedKey(privKey.ID(), "diogomonica") if err == nil { 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 - key, err := rsa.GenerateKey(rand.Reader, 1024) + privKey, err := GenerateRSAKey(rand.Reader, 512) if err != nil { t.Fatalf("could not generate private key: %v", err) } - // Get PEM encodedd key - pemKey, err := KeyToPEM(key) - if err != nil { - t.Fatalf("Could not encode key to PEM: %v", err) - } - // Call the Add function - err = store.AddEncrypted(testName, pemKey, "diogomonica") + // Call the AddEncryptedKey function + err = store.AddEncryptedKey(privKey.ID(), privKey, "diogomonica") if err != nil { t.Fatalf("failed to add file to stoAFre: %v", err) } // Try to decrypt the file with an invalid passphrase - _, err = store.GetDecrypted(testName, "diegomonica") + _, err = store.GetDecryptedKey(testName, "diegomonica") if err == nil { t.Fatalf("expected error while decrypting the content due to invalid passphrase") } diff --git a/trustmanager/x509filestore.go b/trustmanager/x509filestore.go index a9128b0cf1..82c6c1b5cc 100644 --- a/trustmanager/x509filestore.go +++ b/trustmanager/x509filestore.go @@ -5,6 +5,8 @@ import ( "errors" "os" "path" + + "github.com/Sirupsen/logrus" ) // 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) { - fileStore, err := NewFileStore(directory, certExtension) + fileStore, err := NewSimpleFileStore(directory, certExtension) if err != nil { 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. func (s X509FileStore) addNamedCert(cert *x509.Certificate) error { fingerprint := fingerprintCert(cert) - + logrus.Debug("Adding cert with fingerprint: ", fingerprint) // Validate if we already loaded this certificate before if _, ok := s.fingerprintMap[fingerprint]; ok { return errors.New("certificate already in the store") @@ -81,10 +83,12 @@ func (s X509FileStore) addNamedCert(cert *x509.Certificate) error { fileName := fileName(cert) // 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 { return err } + } else if err != nil { + return err } // 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 // an error if no Certificates are found func (s X509FileStore) AddCertFromPEM(pemBytes []byte) error { - cert, err := loadCertFromPEM(pemBytes) + cert, err := LoadCertFromPEM(pemBytes) if err != nil { return err } diff --git a/trustmanager/x509utils.go b/trustmanager/x509utils.go index 9e2eed0c8d..3bfd817d63 100644 --- a/trustmanager/x509utils.go +++ b/trustmanager/x509utils.go @@ -1,16 +1,19 @@ package trustmanager import ( - "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" + "crypto/x509/pkix" "encoding/pem" "errors" "fmt" + "io" "io/ioutil" + "math/big" "net/http" "net/url" + "time" "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 - cert, err := loadCertFromPEM(certBytes) + cert, err := LoadCertFromPEM(certBytes) if err != nil { return nil, err } @@ -56,34 +59,30 @@ func CertToPEM(cert *x509.Certificate) []byte { return pemCert } -// KeyToPEM returns a PEM encoded key from a crypto.PrivateKey -func KeyToPEM(key crypto.PrivateKey) ([]byte, error) { - rsaKey, ok := key.(*rsa.PrivateKey) - if !ok { +// KeyToPEM returns a PEM encoded key from a Private Key +func KeyToPEM(privKey *data.PrivateKey) ([]byte, error) { + if privKey.Cipher() != "RSA" { return nil, errors.New("only RSA keys are currently supported") } - keyBytes := x509.MarshalPKCS1PrivateKey(rsaKey) - return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes}), nil + return pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: privKey.Private()}), nil } -// EncryptPrivateKey returns an encrypted PEM encoded key given a Private key +// EncryptPrivateKey returns an encrypted PEM key given a Privatekey // and a passphrase -func EncryptPrivateKey(key crypto.PrivateKey, passphrase string) ([]byte, error) { - rsaKey, ok := key.(*rsa.PrivateKey) - if !ok { +func EncryptPrivateKey(key *data.PrivateKey, passphrase string) ([]byte, error) { + // TODO(diogo): Currently only supports RSA Private keys + if key.Cipher() != "RSA" { return nil, errors.New("only RSA keys are currently supported") } - keyBytes := x509.MarshalPKCS1PrivateKey(rsaKey) - password := []byte(passphrase) cipherType := x509.PEMCipherAES256 blockType := "RSA PRIVATE KEY" encryptedPEMBlock, err := x509.EncryptPEMBlock(rand.Reader, blockType, - keyBytes, + key.Private(), password, cipherType) if err != nil { @@ -93,9 +92,9 @@ func EncryptPrivateKey(key crypto.PrivateKey, passphrase string) ([]byte, error) 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. -func loadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { +func LoadCertFromPEM(pemBytes []byte) (*x509.Certificate, error) { for len(pemBytes) > 0 { var block *pem.Block block, pemBytes = pem.Decode(pemBytes) @@ -134,7 +133,7 @@ func fingerprintCert(cert *x509.Certificate) CertID { // loadCertsFromDir receives a store AddCertFromFile for each certificate found func loadCertsFromDir(s *X509FileStore) { - certFiles := s.fileStore.ListAll() + certFiles := s.fileStore.ListFiles(true) for _, c := range certFiles { s.AddCertFromFile(c) } @@ -161,39 +160,9 @@ func LoadCertFromFile(filename string) (*x509.Certificate, error) { return nil, errors.New("could not load certificate from file") } -// LoadKeyFromFile returns a PrivateKey given a filename -func LoadKeyFromFile(filename string) (crypto.PrivateKey, error) { - pemBytes, err := ioutil.ReadFile(filename) - 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) { +// ParsePEMPrivateKey returns a data.PrivateKey from a PEM encoded private key. It +// only supports RSA (PKCS#1) and attempts to decrypt using the passphrase, if encrypted. +func ParsePEMPrivateKey(pemBytes []byte, passphrase string) (*data.PrivateKey, error) { block, _ := pem.Decode(pemBytes) if block == nil { return nil, errors.New("no valid private key found") @@ -201,17 +170,82 @@ func ParsePEMEncryptedPrivateKey(pemBytes []byte, passphrase string) (crypto.Pri switch block.Type { case "RSA PRIVATE KEY": - if !x509.IsEncryptedPEMBlock(block) { - return nil, errors.New("private key is not encrypted") + var privKeyBytes []byte + 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 { - 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: 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 +}