Skip to content

Commit b822932

Browse files
authored
Add option to provide signature for a token to verify key ownership (#14054)
* 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. * Ensure verified keys can act for all active emails for the user * Add code to mark keys as verified * Slight UI adjustments * Slight UI adjustments 2 * Simplify signature verification slightly * fix postgres test * add api routes * handle swapped primary-keys * Verify the no-reply address for verified keys * Only add email addresses that are activated to keys * Fix committer shortcut properly * Restructure gpg_keys.go * Use common Verification Token code Signed-off-by: Andrew Thornton <art27@cantab.net>
1 parent 67f135c commit b822932

File tree

20 files changed

+1275
-726
lines changed

20 files changed

+1275
-726
lines changed

integrations/api_gpg_keys_test.go

+4-18
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func TestGPGKeys(t *testing.T) {
2929
results []int
3030
}{
3131
{name: "NoLogin", makeRequest: MakeRequest, token: "",
32-
results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
32+
results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
3333
},
3434
{name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token,
35-
results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusCreated}},
35+
results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusNotFound, http.StatusCreated}},
3636
}
3737

3838
for _, tc := range tt {
@@ -60,7 +60,7 @@ func TestGPGKeys(t *testing.T) {
6060
t.Run("CreateValidGPGKey", func(t *testing.T) {
6161
testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
6262
})
63-
t.Run("CreateValidSecondaryEmailGPGKey", func(t *testing.T) {
63+
t.Run("CreateValidSecondaryEmailGPGKeyNotActivated", func(t *testing.T) {
6464
testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[7])
6565
})
6666
})
@@ -74,6 +74,7 @@ func TestGPGKeys(t *testing.T) {
7474
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) //GET all keys
7575
resp := session.MakeRequest(t, req, http.StatusOK)
7676
DecodeJSON(t, resp, &keys)
77+
assert.Len(t, keys, 1)
7778

7879
primaryKey1 := keys[0] //Primary key 1
7980
assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID)
@@ -85,12 +86,6 @@ func TestGPGKeys(t *testing.T) {
8586
assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID)
8687
assert.Empty(t, subKey.Emails)
8788

88-
primaryKey2 := keys[1] //Primary key 2
89-
assert.EqualValues(t, "3CEF46EF40BEFC3E", primaryKey2.KeyID)
90-
assert.Len(t, primaryKey2.Emails, 1)
91-
assert.EqualValues(t, "user2-2@example.com", primaryKey2.Emails[0].Email)
92-
assert.False(t, primaryKey2.Emails[0].Verified)
93-
9489
var key api.GPGKey
9590
req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey1.ID, 10)+"?token="+token) //Primary key 1
9691
resp = session.MakeRequest(t, req, http.StatusOK)
@@ -105,15 +100,6 @@ func TestGPGKeys(t *testing.T) {
105100
DecodeJSON(t, resp, &key)
106101
assert.EqualValues(t, "70D7C694D17D03AD", key.KeyID)
107102
assert.Empty(t, key.Emails)
108-
109-
req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey2.ID, 10)+"?token="+token) //Primary key 2
110-
resp = session.MakeRequest(t, req, http.StatusOK)
111-
DecodeJSON(t, resp, &key)
112-
assert.EqualValues(t, "3CEF46EF40BEFC3E", key.KeyID)
113-
assert.Len(t, key.Emails, 1)
114-
assert.EqualValues(t, "user2-2@example.com", key.Emails[0].Email)
115-
assert.False(t, key.Emails[0].Verified)
116-
117103
})
118104

119105
//Check state after basic add

models/error.go

+17
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
451451
// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
452452
type ErrGPGNoEmailFound struct {
453453
FailedEmails []string
454+
ID string
454455
}
455456

456457
// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
@@ -463,6 +464,22 @@ func (err ErrGPGNoEmailFound) Error() string {
463464
return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails)
464465
}
465466

467+
// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error.
468+
type ErrGPGInvalidTokenSignature struct {
469+
Wrapped error
470+
ID string
471+
}
472+
473+
// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature.
474+
func IsErrGPGInvalidTokenSignature(err error) bool {
475+
_, ok := err.(ErrGPGInvalidTokenSignature)
476+
return ok
477+
}
478+
479+
func (err ErrGPGInvalidTokenSignature) Error() string {
480+
return "the provided signature does not sign the token with the provided key"
481+
}
482+
466483
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
467484
type ErrGPGKeyParsing struct {
468485
ParseError error

0 commit comments

Comments
 (0)