Skip to content

Commit 33ee748

Browse files
committed
Add option to provide signed token to verify key ownership
Currently we will only allow a key to be matched to a user if it matches an activated email address. This PR provides a different mechanism - if the user provides a signature for automatically generated token (based on the timestamp, user creation time, user ID, username and primary email. Signed-off-by: Andrew Thornton <art27@cantab.net>
1 parent 4aabbac commit 33ee748

File tree

10 files changed

+105
-17
lines changed

10 files changed

+105
-17
lines changed

models/error.go

+17
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
408408
// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
409409
type ErrGPGNoEmailFound struct {
410410
FailedEmails []string
411+
ID string
411412
}
412413

413414
// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
@@ -420,6 +421,22 @@ func (err ErrGPGNoEmailFound) Error() string {
420421
return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails)
421422
}
422423

424+
// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error.
425+
type ErrGPGInvalidTokenSignature struct {
426+
Wrapped error
427+
ID string
428+
}
429+
430+
// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature.
431+
func IsErrGPGInvalidTokenSignature(err error) bool {
432+
_, ok := err.(ErrGPGInvalidTokenSignature)
433+
return ok
434+
}
435+
436+
func (err ErrGPGInvalidTokenSignature) Error() string {
437+
return "the provided signature does not sign the token with the provided key"
438+
}
439+
423440
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
424441
type ErrGPGKeyParsing struct {
425442
ParseError error

models/gpg_key.go

+34-9
Original file line numberDiff line numberDiff line change
@@ -152,17 +152,40 @@ func addGPGSubKey(e Engine, key *GPGKey) (err error) {
152152
}
153153

154154
// AddGPGKey adds new public key to database.
155-
func AddGPGKey(ownerID int64, content string) ([]*GPGKey, error) {
155+
func AddGPGKey(ownerID int64, content, token, signature string) ([]*GPGKey, error) {
156156
ekeys, err := checkArmoredGPGKeyString(content)
157157
if err != nil {
158158
return nil, err
159159
}
160+
160161
sess := x.NewSession()
161162
defer sess.Close()
162163
if err = sess.Begin(); err != nil {
163164
return nil, err
164165
}
165166
keys := make([]*GPGKey, 0, len(ekeys))
167+
168+
signed := false
169+
// Handle provided signature
170+
if signature != "" {
171+
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature))
172+
if err != nil {
173+
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature))
174+
}
175+
if err != nil {
176+
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature))
177+
}
178+
if err != nil {
179+
log.Error("Unable to validate token signature. Error: %v", err)
180+
return nil, ErrGPGInvalidTokenSignature{
181+
ID: ekeys[0].PrimaryKey.KeyIdString(),
182+
Wrapped: err,
183+
}
184+
}
185+
ekeys = []*openpgp.Entity{signer}
186+
signed = true
187+
}
188+
166189
for _, ekey := range ekeys {
167190
// Key ID cannot be duplicated.
168191
has, err := sess.Where("key_id=?", ekey.PrimaryKey.KeyIdString()).
@@ -175,7 +198,7 @@ func AddGPGKey(ownerID int64, content string) ([]*GPGKey, error) {
175198

176199
//Get DB session
177200

178-
key, err := parseGPGKey(ownerID, ekey)
201+
key, err := parseGPGKey(ownerID, ekey, !signed)
179202
if err != nil {
180203
return nil, err
181204
}
@@ -270,7 +293,7 @@ func getExpiryTime(e *openpgp.Entity) time.Time {
270293
}
271294

272295
//parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature)
273-
func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
296+
func parseGPGKey(ownerID int64, e *openpgp.Entity, checkEmail bool) (*GPGKey, error) {
274297
pubkey := e.PrimaryKey
275298
expiry := getExpiryTime(e)
276299

@@ -304,13 +327,15 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
304327
}
305328
}
306329

307-
//In the case no email as been found
308-
if len(emails) == 0 {
309-
failedEmails := make([]string, 0, len(e.Identities))
310-
for _, ident := range e.Identities {
311-
failedEmails = append(failedEmails, ident.UserId.Email)
330+
if checkEmail {
331+
//In the case no email as been found
332+
if len(emails) == 0 {
333+
failedEmails := make([]string, 0, len(e.Identities))
334+
for _, ident := range e.Identities {
335+
failedEmails = append(failedEmails, ident.UserId.Email)
336+
}
337+
return nil, ErrGPGNoEmailFound{failedEmails, e.PrimaryKey.KeyIdString()}
312338
}
313-
return nil, ErrGPGNoEmailFound{failedEmails}
314339
}
315340

316341
content, err := base64EncPubKey(pubkey)

models/gpg_key_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ Q0KHb+QcycSgbDx0ZAvdIacuKvBBcbxrsmFUI4LR+oIup0G9gUc0roPvr014jYQL
220220
=zHo9
221221
-----END PGP PUBLIC KEY BLOCK-----`
222222

223-
keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters)
223+
keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters, "", "")
224224
assert.NoError(t, err)
225225
key := keys[0]
226226
if assert.Len(t, key.Emails, 1) {

modules/auth/user_form.go

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ type AddKeyForm struct {
292292
Type string `binding:"OmitEmpty"`
293293
Title string `binding:"Required;MaxSize(50)"`
294294
Content string `binding:"Required"`
295+
Signature string `binding:"OmitEmpty"`
295296
IsWritable bool
296297
}
297298

modules/structs/user_gpgkey.go

+1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ type CreateGPGKeyOption struct {
4040
// required: true
4141
// unique: true
4242
ArmoredKey string `json:"armored_public_key" binding:"Required"`
43+
Signature string `json:"armored_signature,omitempty"`
4344
}

options/locale/locale_en-US.ini

+6-1
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,12 @@ ssh_key_been_used = This SSH key has already been added to the server.
524524
ssh_key_name_used = An SSH key with same name already exists on your account.
525525
ssh_principal_been_used = This principal has already been added to the server.
526526
gpg_key_id_used = A public GPG key with same ID already exists.
527-
gpg_no_key_email_found = This GPG key is not usable with any email address associated with your account.
527+
gpg_no_key_email_found = This GPG key is not usable with any email address associated with your account. It may still be added if you sign the provided token.
528+
gpg_invalid_token_signature = The provided GPG key, signature and token do not match or token is out-of-date.
529+
gpg_token = You must provide a signature for the following token: '%s'. You can generate a signature with:
530+
gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig
531+
gpg_token_signature = Armored GPG signature
532+
key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----'
528533
subkeys = Subkeys
529534
key_id = Key ID
530535
key_name = Key Name

routers/api/v1/user/gpg_key.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
package user
66

77
import (
8+
"fmt"
89
"net/http"
10+
"strconv"
11+
"time"
912

1013
"code.gitea.io/gitea/models"
14+
"code.gitea.io/gitea/modules/base"
1115
"code.gitea.io/gitea/modules/context"
1216
"code.gitea.io/gitea/modules/convert"
1317
api "code.gitea.io/gitea/modules/structs"
@@ -118,9 +122,11 @@ func GetGPGKey(ctx *context.APIContext) {
118122

119123
// CreateUserGPGKey creates new GPG key to given user by ID.
120124
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
121-
keys, err := models.AddGPGKey(uid, form.ArmoredKey)
125+
token := base.EncodeSha256(time.Now().Round(5*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10))
126+
127+
keys, err := models.AddGPGKey(uid, form.ArmoredKey, token, form.Signature)
122128
if err != nil {
123-
HandleAddGPGKeyError(ctx, err)
129+
HandleAddGPGKeyError(ctx, err, token)
124130
return
125131
}
126132
ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0]))
@@ -187,7 +193,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
187193
}
188194

189195
// HandleAddGPGKeyError handle add GPGKey error
190-
func HandleAddGPGKeyError(ctx *context.APIContext, err error) {
196+
func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) {
191197
switch {
192198
case models.IsErrGPGKeyAccessDenied(err):
193199
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key")
@@ -196,7 +202,9 @@ func HandleAddGPGKeyError(ctx *context.APIContext, err error) {
196202
case models.IsErrGPGKeyParsing(err):
197203
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err)
198204
case models.IsErrGPGNoEmailFound(err):
199-
ctx.Error(http.StatusNotFound, "GPGNoEmailFound", err)
205+
ctx.Error(http.StatusNotFound, "GPGNoEmailFound", fmt.Sprintf("None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: %s", token))
206+
case models.IsErrGPGInvalidTokenSignature(err):
207+
ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token))
200208
default:
201209
ctx.Error(http.StatusInternalServerError, "AddGPGKey", err)
202210
}

routers/user/setting/keys.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
package setting
77

88
import (
9+
"strconv"
10+
"time"
11+
912
"code.gitea.io/gitea/models"
1013
"code.gitea.io/gitea/modules/auth"
1114
"code.gitea.io/gitea/modules/base"
@@ -72,7 +75,9 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) {
7275
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
7376
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
7477
case "gpg":
75-
keys, err := models.AddGPGKey(ctx.User.ID, form.Content)
78+
token := base.EncodeSha256(time.Now().Round(5*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10))
79+
80+
keys, err := models.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature)
7681
if err != nil {
7782
ctx.Data["HasGPGError"] = true
7883
switch {
@@ -84,10 +89,18 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) {
8489

8590
ctx.Data["Err_Content"] = true
8691
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
92+
case models.IsErrGPGInvalidTokenSignature(err):
93+
loadKeysData(ctx)
94+
ctx.Data["Err_Content"] = true
95+
ctx.Data["Err_Signature"] = true
96+
ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID
97+
ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
8798
case models.IsErrGPGNoEmailFound(err):
8899
loadKeysData(ctx)
89100

90101
ctx.Data["Err_Content"] = true
102+
ctx.Data["Err_Signature"] = true
103+
ctx.Data["KeyID"] = err.(models.ErrGPGNoEmailFound).ID
91104
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
92105
default:
93106
ctx.ServerError("AddPublicKey", err)
@@ -194,6 +207,10 @@ func loadKeysData(ctx *context.Context) {
194207
return
195208
}
196209
ctx.Data["GPGKeys"] = gpgkeys
210+
tokenToSign := base.EncodeSha256(time.Now().Round(5*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10))
211+
212+
// generate a new aes cipher using the csrfToken
213+
ctx.Data["TokenToSign"] = tokenToSign
197214

198215
principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
199216
if err != nil {

templates/swagger/v1_json.tmpl

+4
Original file line numberDiff line numberDiff line change
@@ -12032,6 +12032,10 @@
1203212032
"type": "string",
1203312033
"uniqueItems": true,
1203412034
"x-go-name": "ArmoredKey"
12035+
},
12036+
"armored_signature": {
12037+
"type": "string",
12038+
"x-go-name": "Signature"
1203512039
}
1203612040
},
1203712041
"x-go-package": "code.gitea.io/gitea/modules/structs"

templates/user/settings/keys_gpg.tmpl

+11-1
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,23 @@
4242
{{.i18n.Tr "settings.add_new_gpg_key"}}
4343
</h4>
4444
<div class="ui attached segment">
45-
<form class="ui form" action="{{.Link}}" method="post">
45+
<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
4646
{{.CsrfTokenHtml}}
4747
<input type="hidden" name="title" value="none">
4848
<div class="field {{if .Err_Content}}error{{end}}">
4949
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
5050
<textarea id="gpg-key-content" name="content" placeholder="{{.i18n.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
5151
</div>
52+
{{if .Err_Signature}}
53+
<div class="ui error message">
54+
<p>{{.i18n.Tr "settings.gpg_token" .TokenToSign}}</p>
55+
<code>{{.i18n.Tr "settings.gpg_token_code" .TokenToSign .KeyID}}</code>
56+
</div>
57+
<div class="field">
58+
<label for="signature">{{.i18n.Tr "settings.gpg_token_signature"}}</label>
59+
<textarea id="gpg-key-signature" name="signature" placeholder="{{.i18n.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
60+
</div>
61+
{{end}}
5262
<input name="type" type="hidden" value="gpg">
5363
<button class="ui green button">
5464
{{.i18n.Tr "settings.add_key"}}

0 commit comments

Comments
 (0)