Skip to content

Commit ca1c3f1

Browse files
sapklunny
authored andcommitted
Implement GPG api (#710)
* Implement GPG API * Better handle error * Apply review recommendation + simplify database operations * Remove useless comments
1 parent 43c5469 commit ca1c3f1

36 files changed

+7931
-0
lines changed

models/error.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,54 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
245245
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
246246
}
247247

248+
// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
249+
type ErrGPGKeyNotExist struct {
250+
ID int64
251+
}
252+
253+
// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist.
254+
func IsErrGPGKeyNotExist(err error) bool {
255+
_, ok := err.(ErrGPGKeyNotExist)
256+
return ok
257+
}
258+
259+
func (err ErrGPGKeyNotExist) Error() string {
260+
return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
261+
}
262+
263+
// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
264+
type ErrGPGKeyIDAlreadyUsed struct {
265+
KeyID string
266+
}
267+
268+
// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
269+
func IsErrGPGKeyIDAlreadyUsed(err error) bool {
270+
_, ok := err.(ErrGPGKeyIDAlreadyUsed)
271+
return ok
272+
}
273+
274+
func (err ErrGPGKeyIDAlreadyUsed) Error() string {
275+
return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID)
276+
}
277+
278+
// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
279+
type ErrGPGKeyAccessDenied struct {
280+
UserID int64
281+
KeyID int64
282+
}
283+
284+
// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
285+
func IsErrGPGKeyAccessDenied(err error) bool {
286+
_, ok := err.(ErrGPGKeyAccessDenied)
287+
return ok
288+
}
289+
290+
// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
291+
func (err ErrGPGKeyAccessDenied) Error() string {
292+
return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
293+
err.UserID, err.KeyID)
294+
}
295+
248296
// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
249297
type ErrKeyAccessDenied struct {
250298
UserID int64

models/gpg_key.go

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"bytes"
9+
"encoding/base64"
10+
"fmt"
11+
"strings"
12+
"time"
13+
14+
"github.com/go-xorm/xorm"
15+
"golang.org/x/crypto/openpgp"
16+
"golang.org/x/crypto/openpgp/packet"
17+
)
18+
19+
// GPGKey represents a GPG key.
20+
type GPGKey struct {
21+
ID int64 `xorm:"pk autoincr"`
22+
OwnerID int64 `xorm:"INDEX NOT NULL"`
23+
KeyID string `xorm:"INDEX TEXT NOT NULL"`
24+
PrimaryKeyID string `xorm:"TEXT"`
25+
Content string `xorm:"TEXT NOT NULL"`
26+
Created time.Time `xorm:"-"`
27+
CreatedUnix int64
28+
Expired time.Time `xorm:"-"`
29+
ExpiredUnix int64
30+
Added time.Time `xorm:"-"`
31+
AddedUnix int64
32+
SubsKey []*GPGKey `xorm:"-"`
33+
Emails []*EmailAddress
34+
CanSign bool
35+
CanEncryptComms bool
36+
CanEncryptStorage bool
37+
CanCertify bool
38+
}
39+
40+
// BeforeInsert will be invoked by XORM before inserting a record
41+
func (key *GPGKey) BeforeInsert() {
42+
key.AddedUnix = time.Now().Unix()
43+
key.ExpiredUnix = key.Expired.Unix()
44+
key.CreatedUnix = key.Created.Unix()
45+
}
46+
47+
// AfterSet is invoked from XORM after setting the value of a field of this object.
48+
func (key *GPGKey) AfterSet(colName string, _ xorm.Cell) {
49+
switch colName {
50+
case "key_id":
51+
x.Where("primary_key_id=?", key.KeyID).Find(&key.SubsKey)
52+
case "added_unix":
53+
key.Added = time.Unix(key.AddedUnix, 0).Local()
54+
case "expired_unix":
55+
key.Expired = time.Unix(key.ExpiredUnix, 0).Local()
56+
case "created_unix":
57+
key.Created = time.Unix(key.CreatedUnix, 0).Local()
58+
}
59+
}
60+
61+
// ListGPGKeys returns a list of public keys belongs to given user.
62+
func ListGPGKeys(uid int64) ([]*GPGKey, error) {
63+
keys := make([]*GPGKey, 0, 5)
64+
return keys, x.Where("owner_id=? AND primary_key_id=''", uid).Find(&keys)
65+
}
66+
67+
// GetGPGKeyByID returns public key by given ID.
68+
func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
69+
key := new(GPGKey)
70+
has, err := x.Id(keyID).Get(key)
71+
if err != nil {
72+
return nil, err
73+
} else if !has {
74+
return nil, ErrGPGKeyNotExist{keyID}
75+
}
76+
return key, nil
77+
}
78+
79+
// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key.
80+
// The function returns the actual public key on success
81+
func checkArmoredGPGKeyString(content string) (*openpgp.Entity, error) {
82+
list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content))
83+
if err != nil {
84+
return nil, err
85+
}
86+
return list[0], nil
87+
}
88+
89+
//addGPGKey add key and subkeys to database
90+
func addGPGKey(e Engine, key *GPGKey) (err error) {
91+
// Save GPG primary key.
92+
if _, err = e.Insert(key); err != nil {
93+
return err
94+
}
95+
// Save GPG subs key.
96+
for _, subkey := range key.SubsKey {
97+
if err := addGPGKey(e, subkey); err != nil {
98+
return err
99+
}
100+
}
101+
return nil
102+
}
103+
104+
// AddGPGKey adds new public key to database.
105+
func AddGPGKey(ownerID int64, content string) (*GPGKey, error) {
106+
ekey, err := checkArmoredGPGKeyString(content)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
// Key ID cannot be duplicated.
112+
has, err := x.Where("key_id=?", ekey.PrimaryKey.KeyIdString()).
113+
Get(new(GPGKey))
114+
if err != nil {
115+
return nil, err
116+
} else if has {
117+
return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()}
118+
}
119+
120+
//Get DB session
121+
sess := x.NewSession()
122+
defer sessionRelease(sess)
123+
if err = sess.Begin(); err != nil {
124+
return nil, err
125+
}
126+
127+
key, err := parseGPGKey(ownerID, ekey)
128+
if err != nil {
129+
return nil, err
130+
}
131+
132+
if err = addGPGKey(sess, key); err != nil {
133+
return nil, err
134+
}
135+
136+
return key, sess.Commit()
137+
}
138+
139+
//base64EncPubKey encode public kay content to base 64
140+
func base64EncPubKey(pubkey *packet.PublicKey) (string, error) {
141+
var w bytes.Buffer
142+
err := pubkey.Serialize(&w)
143+
if err != nil {
144+
return "", err
145+
}
146+
return base64.StdEncoding.EncodeToString(w.Bytes()), nil
147+
}
148+
149+
//parseSubGPGKey parse a sub Key
150+
func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, expiry time.Time) (*GPGKey, error) {
151+
content, err := base64EncPubKey(pubkey)
152+
if err != nil {
153+
return nil, err
154+
}
155+
return &GPGKey{
156+
OwnerID: ownerID,
157+
KeyID: pubkey.KeyIdString(),
158+
PrimaryKeyID: primaryID,
159+
Content: content,
160+
Created: pubkey.CreationTime,
161+
Expired: expiry,
162+
CanSign: pubkey.CanSign(),
163+
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
164+
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
165+
CanCertify: pubkey.PubKeyAlgo.CanSign(),
166+
}, nil
167+
}
168+
169+
//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature)
170+
func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
171+
pubkey := e.PrimaryKey
172+
173+
//Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165
174+
var selfSig *packet.Signature
175+
for _, ident := range e.Identities {
176+
if selfSig == nil {
177+
selfSig = ident.SelfSignature
178+
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
179+
selfSig = ident.SelfSignature
180+
break
181+
}
182+
}
183+
expiry := time.Time{}
184+
if selfSig.KeyLifetimeSecs != nil {
185+
expiry = selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second)
186+
}
187+
188+
//Parse Subkeys
189+
subkeys := make([]*GPGKey, len(e.Subkeys))
190+
for i, k := range e.Subkeys {
191+
subs, err := parseSubGPGKey(ownerID, pubkey.KeyIdString(), k.PublicKey, expiry)
192+
if err != nil {
193+
return nil, err
194+
}
195+
subkeys[i] = subs
196+
}
197+
198+
//Check emails
199+
userEmails, err := GetEmailAddresses(ownerID)
200+
if err != nil {
201+
return nil, err
202+
}
203+
emails := make([]*EmailAddress, len(e.Identities))
204+
n := 0
205+
for _, ident := range e.Identities {
206+
207+
for _, e := range userEmails {
208+
if e.Email == ident.UserId.Email && e.IsActivated {
209+
emails[n] = e
210+
break
211+
}
212+
}
213+
if emails[n] == nil {
214+
return nil, fmt.Errorf("Failed to found email or is not confirmed : %s", ident.UserId.Email)
215+
}
216+
n++
217+
}
218+
content, err := base64EncPubKey(pubkey)
219+
if err != nil {
220+
return nil, err
221+
}
222+
return &GPGKey{
223+
OwnerID: ownerID,
224+
KeyID: pubkey.KeyIdString(),
225+
PrimaryKeyID: "",
226+
Content: content,
227+
Created: pubkey.CreationTime,
228+
Expired: expiry,
229+
Emails: emails,
230+
SubsKey: subkeys,
231+
CanSign: pubkey.CanSign(),
232+
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
233+
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
234+
CanCertify: pubkey.PubKeyAlgo.CanSign(),
235+
}, nil
236+
}
237+
238+
// deleteGPGKey does the actual key deletion
239+
func deleteGPGKey(e *xorm.Session, keyID string) (int64, error) {
240+
if keyID == "" {
241+
return 0, fmt.Errorf("empty KeyId forbidden") //Should never happen but just to be sure
242+
}
243+
return e.Where("key_id=?", keyID).Or("primary_key_id=?", keyID).Delete(new(GPGKey))
244+
}
245+
246+
// DeleteGPGKey deletes GPG key information in database.
247+
func DeleteGPGKey(doer *User, id int64) (err error) {
248+
key, err := GetGPGKeyByID(id)
249+
if err != nil {
250+
if IsErrGPGKeyNotExist(err) {
251+
return nil
252+
}
253+
return fmt.Errorf("GetPublicKeyByID: %v", err)
254+
}
255+
256+
// Check if user has access to delete this key.
257+
if !doer.IsAdmin && doer.ID != key.OwnerID {
258+
return ErrGPGKeyAccessDenied{doer.ID, key.ID}
259+
}
260+
261+
sess := x.NewSession()
262+
defer sessionRelease(sess)
263+
if err = sess.Begin(); err != nil {
264+
return err
265+
}
266+
267+
if _, err = deleteGPGKey(sess, key.KeyID); err != nil {
268+
return err
269+
}
270+
271+
if err = sess.Commit(); err != nil {
272+
return err
273+
}
274+
275+
return nil
276+
}

models/gpg_key_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2017 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package models
6+
7+
import (
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestCheckArmoredGPGKeyString(t *testing.T) {
14+
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
15+
16+
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
17+
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
18+
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
19+
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
20+
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
21+
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
22+
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
23+
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
24+
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
25+
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
26+
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
27+
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
28+
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
29+
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
30+
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
31+
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
32+
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
33+
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
34+
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
35+
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
36+
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
37+
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
38+
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
39+
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
40+
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
41+
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
42+
=i9b7
43+
-----END PGP PUBLIC KEY BLOCK-----`
44+
45+
key, err := checkArmoredGPGKeyString(testGPGArmor)
46+
assert.Nil(t, err, "Could not parse a valid GPG armored key", key)
47+
//TODO verify value of key
48+
}

models/models.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ func init() {
111111
new(IssueUser),
112112
new(LFSMetaObject),
113113
new(TwoFactor),
114+
new(GPGKey),
114115
new(RepoUnit),
115116
new(RepoRedirect),
116117
new(ExternalLoginUser),

0 commit comments

Comments
 (0)