diff options
author | Antoine GIRARD <sapk@users.noreply.github.com> | 2017-03-16 02:27:35 +0100 |
---|---|---|
committer | Lunny Xiao <xiaolunwen@gmail.com> | 2017-03-16 09:27:35 +0800 |
commit | ca1c3f1926eff992a2458f26cb24ed2f35265b05 (patch) | |
tree | a0c859357539aebf4c3ce521fc8b5d1e6a870364 /models | |
parent | 43c5469f81851c084fa6ac84d8379ae949c3a05c (diff) | |
download | gitea-ca1c3f1926eff992a2458f26cb24ed2f35265b05.tar.gz gitea-ca1c3f1926eff992a2458f26cb24ed2f35265b05.zip |
Implement GPG api (#710)
* Implement GPG API
* Better handle error
* Apply review recommendation + simplify database operations
* Remove useless comments
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 48 | ||||
-rw-r--r-- | models/gpg_key.go | 276 | ||||
-rw-r--r-- | models/gpg_key_test.go | 48 | ||||
-rw-r--r-- | models/models.go | 1 |
4 files changed, 373 insertions, 0 deletions
diff --git a/models/error.go b/models/error.go index 472c8c9426..62529f83fa 100644 --- a/models/error.go +++ b/models/error.go @@ -245,6 +245,54 @@ func (err ErrKeyNameAlreadyUsed) Error() string { return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name) } +// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error. +type ErrGPGKeyNotExist struct { + ID int64 +} + +// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist. +func IsErrGPGKeyNotExist(err error) bool { + _, ok := err.(ErrGPGKeyNotExist) + return ok +} + +func (err ErrGPGKeyNotExist) Error() string { + return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID) +} + +// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error. +type ErrGPGKeyIDAlreadyUsed struct { + KeyID string +} + +// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed. +func IsErrGPGKeyIDAlreadyUsed(err error) bool { + _, ok := err.(ErrGPGKeyIDAlreadyUsed) + return ok +} + +func (err ErrGPGKeyIDAlreadyUsed) Error() string { + return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID) +} + +// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error. +type ErrGPGKeyAccessDenied struct { + UserID int64 + KeyID int64 +} + +// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied. +func IsErrGPGKeyAccessDenied(err error) bool { + _, ok := err.(ErrGPGKeyAccessDenied) + return ok +} + +// Error pretty-prints an error of type ErrGPGKeyAccessDenied. +func (err ErrGPGKeyAccessDenied) Error() string { + return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]", + err.UserID, err.KeyID) +} + // ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error. type ErrKeyAccessDenied struct { UserID int64 diff --git a/models/gpg_key.go b/models/gpg_key.go new file mode 100644 index 0000000000..9ca9a1baf0 --- /dev/null +++ b/models/gpg_key.go @@ -0,0 +1,276 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "bytes" + "encoding/base64" + "fmt" + "strings" + "time" + + "github.com/go-xorm/xorm" + "golang.org/x/crypto/openpgp" + "golang.org/x/crypto/openpgp/packet" +) + +// GPGKey represents a GPG key. +type GPGKey struct { + ID int64 `xorm:"pk autoincr"` + OwnerID int64 `xorm:"INDEX NOT NULL"` + KeyID string `xorm:"INDEX TEXT NOT NULL"` + PrimaryKeyID string `xorm:"TEXT"` + Content string `xorm:"TEXT NOT NULL"` + Created time.Time `xorm:"-"` + CreatedUnix int64 + Expired time.Time `xorm:"-"` + ExpiredUnix int64 + Added time.Time `xorm:"-"` + AddedUnix int64 + SubsKey []*GPGKey `xorm:"-"` + Emails []*EmailAddress + CanSign bool + CanEncryptComms bool + CanEncryptStorage bool + CanCertify bool +} + +// BeforeInsert will be invoked by XORM before inserting a record +func (key *GPGKey) BeforeInsert() { + key.AddedUnix = time.Now().Unix() + key.ExpiredUnix = key.Expired.Unix() + key.CreatedUnix = key.Created.Unix() +} + +// AfterSet is invoked from XORM after setting the value of a field of this object. +func (key *GPGKey) AfterSet(colName string, _ xorm.Cell) { + switch colName { + case "key_id": + x.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey) + case "added_unix": + key.Added = time.Unix(key.AddedUnix, 0).Local() + case "expired_unix": + key.Expired = time.Unix(key.ExpiredUnix, 0).Local() + case "created_unix": + key.Created = time.Unix(key.CreatedUnix, 0).Local() + } +} + +// ListGPGKeys returns a list of public keys belongs to given user. +func ListGPGKeys(uid int64) ([]*GPGKey, error) { + keys := make([]*GPGKey, 0, 5) + return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys) +} + +// GetGPGKeyByID returns public key by given ID. +func GetGPGKeyByID(keyID int64) (*GPGKey, error) { + key := new(GPGKey) + has, err := x.Id(keyID).Get(key) + if err != nil { + return nil, err + } else if !has { + return nil, ErrGPGKeyNotExist{keyID} + } + return key, nil +} + +// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. +// The function returns the actual public key on success +func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) { + list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) + if err != nil { + return nil, err + } + return list[0], nil +} + +//addGPGKey add key and subkeys to database +func addGPGKey(e Engine, key *GPGKey) (err error) { + // Save GPG primary key. + if _, err = e.Insert(key); err != nil { + return err + } + // Save GPG subs key. + for _, subkey := range key.SubsKey { + if err := addGPGKey(e, subkey); err != nil { + return err + } + } + return nil +} + +// AddGPGKey adds new public key to database. +func AddGPGKey(ownerID int64, content string) (*GPGKey, error) { + ekey, err := checkArmoredGPGKeyString(content) + if err != nil { + return nil, err + } + + // Key ID cannot be duplicated. + has, err := x.Where("key_id=?", ekey.PrimaryKey.KeyIdString()). + Get(new(GPGKey)) + if err != nil { + return nil, err + } else if has { + return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()} + } + + //Get DB session + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return nil, err + } + + key, err := parseGPGKey(ownerID, ekey) + if err != nil { + return nil, err + } + + if err = addGPGKey(sess, key); err != nil { + return nil, err + } + + return key, sess.Commit() +} + +//base64EncPubKey encode public kay content to base 64 +func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { + var w bytes.Buffer + err := pubkey.Serialize(&w) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(w.Bytes()), nil +} + +//parseSubGPGKey parse a sub Key +func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) { + content, err := base64EncPubKey(pubkey) + if err != nil { + return nil, err + } + return &GPGKey{ + OwnerID: ownerID, + KeyID: pubkey.KeyIdString(), + PrimaryKeyID: primaryID, + Content: content, + Created: pubkey.CreationTime, + Expired: expiry, + CanSign: pubkey.CanSign(), + CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(), + CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(), + CanCertify: pubkey.PubKeyAlgo.CanSign(), + }, nil +} + +//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature) +func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { + pubkey := e.PrimaryKey + + //Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 + var selfSig *packet.Signature + for _, ident := range e.Identities { + if selfSig == nil { + selfSig = ident.SelfSignature + } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + selfSig = ident.SelfSignature + break + } + } + expiry := time.Time{} + if selfSig.KeyLifetimeSecs != nil { + expiry = selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) + } + + //Parse Subkeys + subkeys := make([]*GPGKey, len(e.Subkeys)) + for i, k := range e.Subkeys { + subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry) + if err != nil { + return nil, err + } + subkeys[i] = subs + } + + //Check emails + userEmails, err := GetEmailAddresses(ownerID) + if err != nil { + return nil, err + } + emails := make([]*EmailAddress, len(e.Identities)) + n := 0 + for _, ident := range e.Identities { + + for _, e := range userEmails { + if e.Email == ident.UserId.Email && e.IsActivated { + emails[n] = e + break + } + } + if emails[n] == nil { + return nil, fmt.Errorf("Failed to found email or is not confirmed : %s", ident.UserId.Email) + } + n++ + } + content, err := base64EncPubKey(pubkey) + if err != nil { + return nil, err + } + return &GPGKey{ + OwnerID: ownerID, + KeyID: pubkey.KeyIdString(), + PrimaryKeyID: "", + Content: content, + Created: pubkey.CreationTime, + Expired: expiry, + Emails: emails, + SubsKey: subkeys, + CanSign: pubkey.CanSign(), + CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(), + CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(), + CanCertify: pubkey.PubKeyAlgo.CanSign(), + }, nil +} + +// deleteGPGKey does the actual key deletion +func deleteGPGKey(e *xorm.Session, keyID string) (int64, error) { + if keyID == "" { + return 0, fmt.Errorf("empty KeyId forbidden") //Should never happen but just to be sure + } + return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey)) +} + +// DeleteGPGKey deletes GPG key information in database. +func DeleteGPGKey(doer *User, id int64) (err error) { + key, err := GetGPGKeyByID(id) + if err != nil { + if IsErrGPGKeyNotExist(err) { + return nil + } + return fmt.Errorf("GetPublicKeyByID: %v", err) + } + + // Check if user has access to delete this key. + if !doer.IsAdmin && doer.ID != key.OwnerID { + return ErrGPGKeyAccessDenied{doer.ID, key.ID} + } + + sess := x.NewSession() + defer sessionRelease(sess) + if err = sess.Begin(); err != nil { + return err + } + + if _, err = deleteGPGKey(sess, key.KeyID); err != nil { + return err + } + + if err = sess.Commit(); err != nil { + return err + } + + return nil +} diff --git a/models/gpg_key_test.go b/models/gpg_key_test.go new file mode 100644 index 0000000000..1ef5838e31 --- /dev/null +++ b/models/gpg_key_test.go @@ -0,0 +1,48 @@ +// Copyright 2017 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckArmoredGPGKeyString(t *testing.T) { + testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv +z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m +/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1 +vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN +0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac +mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE +IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF +Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY +KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa +MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ +ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+ +sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo +T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i +iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE +QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT +pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU +JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN +/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx +ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02 +cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF +CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH +6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk +lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo +RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP +Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR +MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg== +=i9b7 +-----END PGP PUBLIC KEY BLOCK-----` + + key, err := checkArmoredGPGKeyString(testGPGArmor) + assert.Nil(t, err, "Could not parse a valid GPG armored key", key) + //TODO verify value of key +} diff --git a/models/models.go b/models/models.go index 5b9020bac6..bba4446db0 100644 --- a/models/models.go +++ b/models/models.go @@ -111,6 +111,7 @@ func init() { new(IssueUser), new(LFSMetaObject), new(TwoFactor), + new(GPGKey), new(RepoUnit), new(RepoRedirect), new(ExternalLoginUser), |