aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--integrations/user_test.go87
-rw-r--r--models/error.go15
-rw-r--r--models/fixtures/gpg_key_import.yml1
-rw-r--r--models/gpg_key.go97
-rw-r--r--models/migrations/migrations.go2
-rw-r--r--models/migrations/v84.go18
-rw-r--r--models/models.go1
-rw-r--r--models/user.go2
-rw-r--r--routers/user/home.go41
-rw-r--r--routers/user/profile.go15
10 files changed, 259 insertions, 20 deletions
diff --git a/integrations/user_test.go b/integrations/user_test.go
index a6ad164d61..fd25f1c570 100644
--- a/integrations/user_test.go
+++ b/integrations/user_test.go
@@ -101,3 +101,90 @@ func TestRenameReservedUsername(t *testing.T) {
models.AssertNotExistsBean(t, &models.User{Name: reservedUsername})
}
}
+
+func TestExportUserGPGKeys(t *testing.T) {
+ prepareTestEnv(t)
+ //Export empty key list
+ testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+
+=twTO
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+ //Import key
+ //User1 <user1@example.com>
+ session := loginUser(t, "user1")
+ token := getTokenForLoggedInUser(t, session)
+ testCreateGPGKey(t, session.MakeRequest, token, http.StatusCreated, `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFyy/VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
+QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh/6eFSRrjsusp3YQ/08NSfPPbcu8
+0M5G+VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
+8SR+lzp5n6ppUakcmRnxt3nGRBj1+hEGkdgzyPo93iy+WioegY2lwCA9xMEo5dah
+BmYxWx51zyiXYlReTaxlyb3/nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
+510OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAG0GVVzZXIxIDx1c2VyMUBl
+eGFtcGxlLmNvbT6JAU4EEwEIADgWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9
+VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9+v0I6RSEH22YCACFqL5+
+6M0m18AMC/pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
+u7+j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
+rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj/ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
+nsdSgyCkwRZXG+u3kT/wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
+96ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9+OsfJVC
+l7N5xxIawCuTQdbfuQENBFyy/VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
+soO/HPj9dPQLTOiwXgSgSCd8C+LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
+55aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU/HVz5y
+lPzxUUocgdbSi3GE3zbzijQzVJdyL/kw/KP7pKT/PPKKJ2C5NQDLy0XGKEHddXGR
+EWKkVlRalxq/TjfaMR0bi3MpezBsQmp99ATPO/d7trayZUxQHRtXzGFiOXfDHATr
+qN730sODjqvU+mpc/SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ/ABEBAAGJATYE
+GAEIACAWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9VQIbDAAKCRD9+v0I6RSE
+H7WoB/4tXl+97rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
+C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
+21dccpqchByVw/UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy/n2
+0Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61+Vr2GUbah6
+7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
+GrE0MHOxUbc9tbtyk0F1SuzREUBH
+=DDXw
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+ //Export new key
+ testExportUserGPGKeys(t, "user1", `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+xsBNBFyy/VUBCADJ7zbM20Z1RWmFoVgp5WkQfI2rU1Vj9cQHes9i42wVLLtcbPeo
+QzubgzvMPITDy7nfWxgSf83E23DoHQ1ACFbQh/6eFSRrjsusp3YQ/08NSfPPbcu8
+0M5G+VGwSfzS5uEcwBVQmHyKdcOZIERTNMtYZx1C3bjLD1XVJHvWz9D72Uq4qeO3
+8SR+lzp5n6ppUakcmRnxt3nGRBj1+hEGkdgzyPo93iy+WioegY2lwCA9xMEo5dah
+BmYxWx51zyiXYlReTaxlyb3/nuSUt8IcW3Q8zjdtJj4Nu8U1SpV8EdaA1I9IPbHW
+510OSLmD3XhqHH5m6mIxL1YoWxk3V7gpDROtABEBAAHNGVVzZXIxIDx1c2VyMUBl
+eGFtcGxlLmNvbT7CwI4EEwEIADgWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9
+VQIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRD9+v0I6RSEH22YCACFqL5+
+6M0m18AMC/pumcpnnmvAS1GrrKTF8nOROA1augZwp1WCNuKw2R6uOJIHANrYECSn
+u7+j6GBP2gbIW8mSAzS6HWCs7GGiPpVtT4wcu8wljUI6BxjpyZtoEkriyBjt6HfK
+rkegbkuySoJvjq4IcO5D1LB1JWgsUjMYQJj/ZpBIzVtjG9QtFSOiT1Hct4PoZHdC
+nsdSgyCkwRZXG+u3kT/wP9F663ba4o16vYlz3dCGo66lF2tyoG3qcyZ1OUzUrnuv
+96ytAzT6XIhrE0nVoBprMxFF5zExotJD3bHjcGBFNLf944bhjKee3U6t9+OsfJVC
+l7N5xxIawCuTQdbfzsBNBFyy/VUBCADe61yGEoTwKfsOKIhxLaNoRmD883O0tiWt
+soO/HPj9dPQLTOiwXgSgSCd8C+LNxGKct87wgFozpah4tDLC6c0nALuHJ0SLbkfz
+55aRhLeOOcrAydatDp72GroXzqpZ0xZBk5wjIWdgEol2GmVRM8QGbeuakU/HVz5y
+lPzxUUocgdbSi3GE3zbzijQzVJdyL/kw/KP7pKT/PPKKJ2C5NQDLy0XGKEHddXGR
+EWKkVlRalxq/TjfaMR0bi3MpezBsQmp99ATPO/d7trayZUxQHRtXzGFiOXfDHATr
+qN730sODjqvU+mpc/SHCRwh9qWDjZRHSuKU5YDBjb5jIQJivZsQ/ABEBAAHCwHYE
+GAEIACAWIQTQEbrYxmXsp1z3j7z9+v0I6RSEHwUCXLL9VQIbDAAKCRD9+v0I6RSE
+H7WoB/4tXl+97rQ6owPCGSVp1Xbwt2521V7COgsOFRVTRTryEWxRW8mm0S7wQvax
+C0TLXKur6NVYQMn01iyL+FZzRpEWNuYF3f9QeeLJ/+l2DafESNhNTy17+RPmacK6
+21dccpqchByVw/UMDeHSyjQLiG2lxzt8Gfx2gHmSbrq3aWovTGyz6JTffZvfy/n2
+0Hm437OBPazO0gZyXhdV2PE5RSUfvAgm44235tcV5EV0d32TJDfv61+Vr2GUbah6
+7XhJ1v6JYuh8kaYaEz8OpZDeh7f6Ho6PzJrsy/TKTKhGgZNINj1iaPFyOkQgKR5M
+GrE0MHOxUbc9tbtyk0F1SuzREUBH
+=WFf5
+-----END PGP PUBLIC KEY BLOCK-----
+`)
+}
+
+func testExportUserGPGKeys(t *testing.T, user, expected string) {
+ session := loginUser(t, user)
+ t.Logf("Testing username %s export gpg keys", user)
+ req := NewRequest(t, "GET", "/"+user+".gpg")
+ resp := session.MakeRequest(t, req, http.StatusOK)
+ //t.Log(resp.Body.String())
+ assert.Equal(t, expected, resp.Body.String())
+}
diff --git a/models/error.go b/models/error.go
index 6a135bda1a..f079af4e1f 100644
--- a/models/error.go
+++ b/models/error.go
@@ -379,6 +379,21 @@ func (err ErrGPGKeyNotExist) Error() string {
return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
}
+// ErrGPGKeyImportNotExist represents a "GPGKeyImportNotExist" kind of error.
+type ErrGPGKeyImportNotExist struct {
+ ID string
+}
+
+// IsErrGPGKeyImportNotExist checks if an error is a ErrGPGKeyImportNotExist.
+func IsErrGPGKeyImportNotExist(err error) bool {
+ _, ok := err.(ErrGPGKeyImportNotExist)
+ return ok
+}
+
+func (err ErrGPGKeyImportNotExist) Error() string {
+ return fmt.Sprintf("public gpg key import does not exist [id: %s]", err.ID)
+}
+
// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
type ErrGPGKeyIDAlreadyUsed struct {
KeyID string
diff --git a/models/fixtures/gpg_key_import.yml b/models/fixtures/gpg_key_import.yml
new file mode 100644
index 0000000000..ca780a73aa
--- /dev/null
+++ b/models/fixtures/gpg_key_import.yml
@@ -0,0 +1 @@
+[] # empty
diff --git a/models/gpg_key.go b/models/gpg_key.go
index 0352456e54..2e10fd782a 100644
--- a/models/gpg_key.go
+++ b/models/gpg_key.go
@@ -43,6 +43,12 @@ type GPGKey struct {
CanCertify bool
}
+//GPGKeyImport the original import of key
+type GPGKeyImport struct {
+ KeyID string `xorm:"pk CHAR(16) NOT NULL"`
+ Content string `xorm:"TEXT NOT NULL"`
+}
+
// BeforeInsert will be invoked by XORM before inserting a record
func (key *GPGKey) BeforeInsert() {
key.AddedUnix = util.TimeStampNow()
@@ -74,6 +80,18 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
return key, nil
}
+// GetGPGImportByKeyID returns the import public armored key by given KeyID.
+func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
+ key := new(GPGKeyImport)
+ has, err := x.ID(keyID).Get(key)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrGPGKeyImportNotExist{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) {
@@ -84,15 +102,37 @@ func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
return list[0], nil
}
-//addGPGKey add key and subkeys to database
-func addGPGKey(e Engine, key *GPGKey) (err error) {
+//addGPGKey add key, import and subkeys to database
+func addGPGKey(e Engine, key *GPGKey, content string) (err error) {
+ //Add GPGKeyImport
+ if _, err = e.Insert(GPGKeyImport{
+ KeyID: key.KeyID,
+ Content: content,
+ }); err != nil {
+ return err
+ }
+ // Save GPG primary key.
+ if _, err = e.Insert(key); err != nil {
+ return err
+ }
+ // Save GPG subs key.
+ for _, subkey := range key.SubsKey {
+ if err := addGPGSubKey(e, subkey); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+//addGPGSubKey add subkeys to database
+func addGPGSubKey(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 {
+ if err := addGPGSubKey(e, subkey); err != nil {
return err
}
}
@@ -127,14 +167,14 @@ func AddGPGKey(ownerID int64, content string) (*GPGKey, error) {
return nil, err
}
- if err = addGPGKey(sess, key); err != nil {
+ if err = addGPGKey(sess, key, content); err != nil {
return nil, err
}
return key, sess.Commit()
}
-//base64EncPubKey encode public kay content to base 64
+//base64EncPubKey encode public key content to base 64
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
var w bytes.Buffer
err := pubkey.Serialize(&w)
@@ -144,6 +184,34 @@ func base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
return base64.StdEncoding.EncodeToString(w.Bytes()), nil
}
+//base64DecPubKey decode public key content from base 64
+func base64DecPubKey(content string) (*packet.PublicKey, error) {
+ b, err := readerFromBase64(content)
+ if err != nil {
+ return nil, err
+ }
+ //Read key
+ p, err := packet.Read(b)
+ if err != nil {
+ return nil, err
+ }
+ //Check type
+ pkey, ok := p.(*packet.PublicKey)
+ if !ok {
+ return nil, fmt.Errorf("key is not a public key")
+ }
+ return pkey, nil
+}
+
+//GPGKeyToEntity retrieve the imported key and the traducted entity
+func GPGKeyToEntity(k *GPGKey) (*openpgp.Entity, error) {
+ impKey, err := GetGPGImportByKeyID(k.KeyID)
+ if err != nil {
+ return nil, err
+ }
+ return checkArmoredGPGKeyString(impKey.Content)
+}
+
//parseSubGPGKey parse a sub Key
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) {
content, err := base64EncPubKey(pubkey)
@@ -244,6 +312,11 @@ 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
}
+ //Delete imported key
+ n, err := e.Where("key_id=?", keyID).Delete(new(GPGKeyImport))
+ if err != nil {
+ return n, err
+ }
return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey))
}
@@ -339,22 +412,10 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
return fmt.Errorf("key can not sign")
}
//Decode key
- b, err := readerFromBase64(k.Content)
- if err != nil {
- return err
- }
- //Read key
- p, err := packet.Read(b)
+ pkey, err := base64DecPubKey(k.Content)
if err != nil {
return err
}
-
- //Check type
- pkey, ok := p.(*packet.PublicKey)
- if !ok {
- return fmt.Errorf("key is not a public key")
- }
-
return pkey.VerifySignature(h, s)
}
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index baedcbb715..62c41fb906 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -221,6 +221,8 @@ var migrations = []Migration{
NewMigration("hot fix for wrong release sha1 on release table", fixReleaseSha1OnReleaseTable),
// v83 -> v84
NewMigration("add uploader id for table attachment", addUploaderIDForAttachment),
+ // v84 -> v85
+ NewMigration("add table to store original imported gpg keys", addGPGKeyImport),
}
// Migrate database to current version
diff --git a/models/migrations/v84.go b/models/migrations/v84.go
new file mode 100644
index 0000000000..4acb94b9ce
--- /dev/null
+++ b/models/migrations/v84.go
@@ -0,0 +1,18 @@
+// Copyright 2019 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 migrations
+
+import (
+ "github.com/go-xorm/xorm"
+)
+
+func addGPGKeyImport(x *xorm.Engine) error {
+ type GPGKeyImport struct {
+ KeyID string `xorm:"pk CHAR(16) NOT NULL"`
+ Content string `xorm:"TEXT NOT NULL"`
+ }
+
+ return x.Sync2(new(GPGKeyImport))
+}
diff --git a/models/models.go b/models/models.go
index e7ecc67fc5..352c07e0c3 100644
--- a/models/models.go
+++ b/models/models.go
@@ -108,6 +108,7 @@ func init() {
new(LFSMetaObject),
new(TwoFactor),
new(GPGKey),
+ new(GPGKeyImport),
new(RepoUnit),
new(RepoRedirect),
new(ExternalLoginUser),
diff --git a/models/user.go b/models/user.go
index 723892b0b6..93fdc6f4af 100644
--- a/models/user.go
+++ b/models/user.go
@@ -747,7 +747,7 @@ var (
".",
"..",
}
- reservedUserPatterns = []string{"*.keys"}
+ reservedUserPatterns = []string{"*.keys", "*.gpg"}
)
// isUsableName checks if name is reserved or pattern of name is not allowed
diff --git a/routers/user/home.go b/routers/user/home.go
index 740a9edc4e..8eedeb70bd 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -19,6 +19,8 @@ import (
"github.com/Unknwon/com"
"github.com/Unknwon/paginater"
+ "github.com/keybase/go-crypto/openpgp"
+ "github.com/keybase/go-crypto/openpgp/armor"
)
const (
@@ -384,6 +386,45 @@ func ShowSSHKeys(ctx *context.Context, uid int64) {
ctx.PlainText(200, buf.Bytes())
}
+// ShowGPGKeys output all the public GPG keys of user by uid
+func ShowGPGKeys(ctx *context.Context, uid int64) {
+ keys, err := models.ListGPGKeys(uid)
+ if err != nil {
+ ctx.ServerError("ListGPGKeys", err)
+ return
+ }
+ entities := make([]*openpgp.Entity, 0)
+ failedEntitiesID := make([]string, 0)
+ for _, k := range keys {
+ e, err := models.GPGKeyToEntity(k)
+ if err != nil {
+ if models.IsErrGPGKeyImportNotExist(err) {
+ failedEntitiesID = append(failedEntitiesID, k.KeyID)
+ continue //Skip previous import without backup of imported armored key
+ }
+ ctx.ServerError("ShowGPGKeys", err)
+ return
+ }
+ entities = append(entities, e)
+ }
+ var buf bytes.Buffer
+
+ headers := make(map[string]string)
+ if len(failedEntitiesID) > 0 { //If some key need re-import to be exported
+ headers["Note"] = fmt.Sprintf("The keys with the following IDs couldn't be exported and need to be reuploaded %s", strings.Join(failedEntitiesID, ", "))
+ }
+ writer, _ := armor.Encode(&buf, "PGP PUBLIC KEY BLOCK", headers)
+ for _, e := range entities {
+ err = e.Serialize(writer) //TODO find why key are exported with a different cipherTypeByte as original (should not be blocking but strange)
+ if err != nil {
+ ctx.ServerError("ShowGPGKeys", err)
+ return
+ }
+ }
+ writer.Close()
+ ctx.PlainText(200, buf.Bytes())
+}
+
func showOrgProfile(ctx *context.Context) {
ctx.SetParams(":org", ctx.Params(":username"))
context.HandleOrgAssignment(ctx)
diff --git a/routers/user/profile.go b/routers/user/profile.go
index 03f88e256a..675c1dc3f4 100644
--- a/routers/user/profile.go
+++ b/routers/user/profile.go
@@ -59,9 +59,16 @@ func Profile(ctx *context.Context) {
isShowKeys := false
if strings.HasSuffix(uname, ".keys") {
isShowKeys = true
+ uname = strings.TrimSuffix(uname, ".keys")
}
- ctxUser := GetUserByName(ctx, strings.TrimSuffix(uname, ".keys"))
+ isShowGPG := false
+ if strings.HasSuffix(uname, ".gpg") {
+ isShowGPG = true
+ uname = strings.TrimSuffix(uname, ".gpg")
+ }
+
+ ctxUser := GetUserByName(ctx, uname)
if ctx.Written() {
return
}
@@ -72,6 +79,12 @@ func Profile(ctx *context.Context) {
return
}
+ // Show GPG keys.
+ if isShowGPG {
+ ShowGPGKeys(ctx, ctxUser.ID)
+ return
+ }
+
if ctxUser.IsOrganization() {
showOrgProfile(ctx)
return