Skip to content

Commit 44c85b9

Browse files
committed
Implement GPG API
1 parent 1ccdf19 commit 44c85b9

36 files changed

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

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)