From 33ee7488805da4e0b2862e93c870bfb1fb08bcc6 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 18 Dec 2020 17:43:13 +0000 Subject: [PATCH 01/26] 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 --- models/error.go | 17 +++++++++++ models/gpg_key.go | 43 +++++++++++++++++++++------ models/gpg_key_test.go | 2 +- modules/auth/user_form.go | 1 + modules/structs/user_gpgkey.go | 1 + options/locale/locale_en-US.ini | 7 ++++- routers/api/v1/user/gpg_key.go | 16 +++++++--- routers/user/setting/keys.go | 19 +++++++++++- templates/swagger/v1_json.tmpl | 4 +++ templates/user/settings/keys_gpg.tmpl | 12 +++++++- 10 files changed, 105 insertions(+), 17 deletions(-) diff --git a/models/error.go b/models/error.go index 7f1eda1b14eb5..0b2aea662c8fb 100644 --- a/models/error.go +++ b/models/error.go @@ -408,6 +408,7 @@ func (err ErrKeyNameAlreadyUsed) Error() string { // ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error. type ErrGPGNoEmailFound struct { FailedEmails []string + ID string } // IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound. @@ -420,6 +421,22 @@ func (err ErrGPGNoEmailFound) Error() string { return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails) } +// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error. +type ErrGPGInvalidTokenSignature struct { + Wrapped error + ID string +} + +// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature. +func IsErrGPGInvalidTokenSignature(err error) bool { + _, ok := err.(ErrGPGInvalidTokenSignature) + return ok +} + +func (err ErrGPGInvalidTokenSignature) Error() string { + return "the provided signature does not sign the token with the provided key" +} + // ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error. type ErrGPGKeyParsing struct { ParseError error diff --git a/models/gpg_key.go b/models/gpg_key.go index b944fdcbffe45..020b6c9ba8bee 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -152,17 +152,40 @@ func addGPGSubKey(e Engine, key *GPGKey) (err error) { } // AddGPGKey adds new public key to database. -func AddGPGKey(ownerID int64, content string) ([]*GPGKey, error) { +func AddGPGKey(ownerID int64, content, token, signature string) ([]*GPGKey, error) { ekeys, err := checkArmoredGPGKeyString(content) if err != nil { return nil, err } + sess := x.NewSession() defer sess.Close() if err = sess.Begin(); err != nil { return nil, err } keys := make([]*GPGKey, 0, len(ekeys)) + + signed := false + // Handle provided signature + if signature != "" { + signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) + if err != nil { + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) + } + if err != nil { + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) + } + if err != nil { + log.Error("Unable to validate token signature. Error: %v", err) + return nil, ErrGPGInvalidTokenSignature{ + ID: ekeys[0].PrimaryKey.KeyIdString(), + Wrapped: err, + } + } + ekeys = []*openpgp.Entity{signer} + signed = true + } + for _, ekey := range ekeys { // Key ID cannot be duplicated. has, err := sess.Where("key_id=?", ekey.PrimaryKey.KeyIdString()). @@ -175,7 +198,7 @@ func AddGPGKey(ownerID int64, content string) ([]*GPGKey, error) { //Get DB session - key, err := parseGPGKey(ownerID, ekey) + key, err := parseGPGKey(ownerID, ekey, !signed) if err != nil { return nil, err } @@ -270,7 +293,7 @@ func getExpiryTime(e *openpgp.Entity) time.Time { } //parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature) -func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { +func parseGPGKey(ownerID int64, e *openpgp.Entity, checkEmail bool) (*GPGKey, error) { pubkey := e.PrimaryKey expiry := getExpiryTime(e) @@ -304,13 +327,15 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { } } - //In the case no email as been found - if len(emails) == 0 { - failedEmails := make([]string, 0, len(e.Identities)) - for _, ident := range e.Identities { - failedEmails = append(failedEmails, ident.UserId.Email) + if checkEmail { + //In the case no email as been found + if len(emails) == 0 { + failedEmails := make([]string, 0, len(e.Identities)) + for _, ident := range e.Identities { + failedEmails = append(failedEmails, ident.UserId.Email) + } + return nil, ErrGPGNoEmailFound{failedEmails, e.PrimaryKey.KeyIdString()} } - return nil, ErrGPGNoEmailFound{failedEmails} } content, err := base64EncPubKey(pubkey) diff --git a/models/gpg_key_test.go b/models/gpg_key_test.go index 92131f5976663..aea132fe2a177 100644 --- a/models/gpg_key_test.go +++ b/models/gpg_key_test.go @@ -220,7 +220,7 @@ Q0KHb+QcycSgbDx0ZAvdIacuKvBBcbxrsmFUI4LR+oIup0G9gUc0roPvr014jYQL =zHo9 -----END PGP PUBLIC KEY BLOCK-----` - keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters) + keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters, "", "") assert.NoError(t, err) key := keys[0] if assert.Len(t, key.Emails, 1) { diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go index c0aafec9e4a9e..d00de179137d5 100644 --- a/modules/auth/user_form.go +++ b/modules/auth/user_form.go @@ -292,6 +292,7 @@ type AddKeyForm struct { Type string `binding:"OmitEmpty"` Title string `binding:"Required;MaxSize(50)"` Content string `binding:"Required"` + Signature string `binding:"OmitEmpty"` IsWritable bool } diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go index f501a09cb92da..1a4894f74dcae 100644 --- a/modules/structs/user_gpgkey.go +++ b/modules/structs/user_gpgkey.go @@ -40,4 +40,5 @@ type CreateGPGKeyOption struct { // required: true // unique: true ArmoredKey string `json:"armored_public_key" binding:"Required"` + Signature string `json:"armored_signature,omitempty"` } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 63a768dc737d9..6f847da9225d3 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -524,7 +524,12 @@ ssh_key_been_used = This SSH key has already been added to the server. ssh_key_name_used = An SSH key with same name already exists on your account. ssh_principal_been_used = This principal has already been added to the server. gpg_key_id_used = A public GPG key with same ID already exists. -gpg_no_key_email_found = This GPG key is not usable with any email address associated with your account. +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. +gpg_invalid_token_signature = The provided GPG key, signature and token do not match or token is out-of-date. +gpg_token = You must provide a signature for the following token: '%s'. You can generate a signature with: +gpg_token_code = echo "%s" | gpg -a --default-key %s --detach-sig +gpg_token_signature = Armored GPG signature +key_signature_gpg_placeholder = Begins with '-----BEGIN PGP SIGNATURE-----' subkeys = Subkeys key_id = Key ID key_name = Key Name diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 7290ef485a1cb..b7411ca01e3be 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -5,9 +5,13 @@ package user import ( + "fmt" "net/http" + "strconv" + "time" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -118,9 +122,11 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { - keys, err := models.AddGPGKey(uid, form.ArmoredKey) + 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)) + + keys, err := models.AddGPGKey(uid, form.ArmoredKey, token, form.Signature) if err != nil { - HandleAddGPGKeyError(ctx, err) + HandleAddGPGKeyError(ctx, err, token) return } ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0])) @@ -187,7 +193,7 @@ func DeleteGPGKey(ctx *context.APIContext) { } // HandleAddGPGKeyError handle add GPGKey error -func HandleAddGPGKeyError(ctx *context.APIContext, err error) { +func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) { switch { case models.IsErrGPGKeyAccessDenied(err): 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) { case models.IsErrGPGKeyParsing(err): ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err) case models.IsErrGPGNoEmailFound(err): - ctx.Error(http.StatusNotFound, "GPGNoEmailFound", err) + 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)) + case models.IsErrGPGInvalidTokenSignature(err): + 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)) default: ctx.Error(http.StatusInternalServerError, "AddGPGKey", err) } diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go index 6a39666e944bb..7b82da0460ba0 100644 --- a/routers/user/setting/keys.go +++ b/routers/user/setting/keys.go @@ -6,6 +6,9 @@ package setting import ( + "strconv" + "time" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/auth" "code.gitea.io/gitea/modules/base" @@ -72,7 +75,9 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) { ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": - keys, err := models.AddGPGKey(ctx.User.ID, form.Content) + 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)) + + keys, err := models.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature) if err != nil { ctx.Data["HasGPGError"] = true switch { @@ -84,10 +89,18 @@ func KeysPost(ctx *context.Context, form auth.AddKeyForm) { ctx.Data["Err_Content"] = true ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) + case models.IsErrGPGInvalidTokenSignature(err): + loadKeysData(ctx) + ctx.Data["Err_Content"] = true + ctx.Data["Err_Signature"] = true + ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID + ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) case models.IsErrGPGNoEmailFound(err): loadKeysData(ctx) ctx.Data["Err_Content"] = true + ctx.Data["Err_Signature"] = true + ctx.Data["KeyID"] = err.(models.ErrGPGNoEmailFound).ID ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) default: ctx.ServerError("AddPublicKey", err) @@ -194,6 +207,10 @@ func loadKeysData(ctx *context.Context) { return } ctx.Data["GPGKeys"] = gpgkeys + 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)) + + // generate a new aes cipher using the csrfToken + ctx.Data["TokenToSign"] = tokenToSign principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{}) if err != nil { diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index aa31b3f07892e..2f0fcff73a1e0 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -12032,6 +12032,10 @@ "type": "string", "uniqueItems": true, "x-go-name": "ArmoredKey" + }, + "armored_signature": { + "type": "string", + "x-go-name": "Signature" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index 91658a15ee31d..2066d6a2e45e1 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -42,13 +42,23 @@ {{.i18n.Tr "settings.add_new_gpg_key"}}
-
+ {{.CsrfTokenHtml}}
+ {{if .Err_Signature}} +
+

{{.i18n.Tr "settings.gpg_token" .TokenToSign}}

+ {{.i18n.Tr "settings.gpg_token_code" .TokenToSign .KeyID}} +
+
+ + +
+ {{end}} + {{if and (not .Verified) (ne $.VerifyingID .ID)}} + {{$.i18n.Tr "settings.gpg_key_verify"}} + {{end}}
{{svg "octicon-key" 32}} @@ -30,8 +33,40 @@ - {{if not .ExpiredUnix.IsZero}}{{$.i18n.Tr "settings.valid_until"}} {{.ExpiredUnix.FormatShort}}{{else}}{{$.i18n.Tr "settings.valid_forever"}}{{end}}
+ {{if .Verified}}{{svg "octicon-shield-check" 16 }} {{$.i18n.Tr "settings.gpg_key_verified"}}{{end}} + {{if and (not .Verified) (eq $.VerifyingID .ID)}} +
+

{{$.i18n.Tr "settings.gpg_token_required"}}

+ + {{$.CsrfTokenHtml}} + + + +
+ + +
+

{{$.i18n.Tr "settings.gpg_token_help"}}

+

{{$.i18n.Tr "settings.gpg_token_code" $.TokenToSign .KeyID}}

+
+
+
+
+ + +
+ + + + {{$.i18n.Tr "settings.cancel"}} + + +
+ {{end}} {{end}} @@ -51,8 +86,15 @@ {{if .Err_Signature}}
-

{{.i18n.Tr "settings.gpg_token" .TokenToSign}}

- {{.i18n.Tr "settings.gpg_token_code" .TokenToSign .KeyID}} +

{{.i18n.Tr "settings.gpg_token_required"}}

+
+
+
From 0ece4931f99b2926efc184dead135cfcd100138c Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 3 Mar 2021 09:28:53 +0000 Subject: [PATCH 04/26] Slight UI adjustments Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 5 ++++- templates/user/settings/keys_gpg.tmpl | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index e3045d9d21629..9f58467371788 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -535,7 +535,10 @@ ssh_key_name_used = An SSH key with same name already exists on your account. ssh_principal_been_used = This principal has already been added to the server. gpg_key_id_used = A public GPG key with same ID already exists. 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. -gpg_key_verified=Key has been verified with a token +gpg_key_matched_identities = Matched Identities: +gpg_key_matched_identities_long=The embedded identities in this key match the following activated email addresses for this user and commits with matching these email addresses can be verified with this key. +gpg_key_verified=Verified Key +gpg_key_verified_long=Key has been verified with a token and can be used to verify commits matching any activated email addresses for this user in addition to any matched identities for this key. gpg_key_verify=Verify gpg_invalid_token_signature = The provided GPG key, signature and token do not match or token is out-of-date. gpg_token_required = You must provide a signature for the below token diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index a8cbfa3094fd7..2a82a9797844b 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -23,7 +23,12 @@ {{svg "octicon-key" 32}}
- {{range .Emails}}{{.Email}} {{end}} + {{if .Verified}} + {{svg "octicon-shield-check"}} {{$.i18n.Tr "settings.gpg_key_verified"}} + {{end}} + {{if gt (len .Emails) 0}} + {{svg "octicon-mail"}} {{$.i18n.Tr "settings.gpg_key_matched_identities"}} {{range .Emails}}{{.Email}} {{end}} + {{end}}
{{$.i18n.Tr "settings.key_id"}}: {{.KeyID}} {{$.i18n.Tr "settings.subkeys"}}: {{range .SubsKey}} {{.KeyID}} {{end}} @@ -33,7 +38,6 @@ - {{if not .ExpiredUnix.IsZero}}{{$.i18n.Tr "settings.valid_until"}} {{.ExpiredUnix.FormatShort}}{{else}}{{$.i18n.Tr "settings.valid_forever"}}{{end}}
- {{if .Verified}}{{svg "octicon-shield-check" 16 }} {{$.i18n.Tr "settings.gpg_key_verified"}}{{end}}
{{if and (not .Verified) (eq $.VerifyingID .ID)}} From 175c0037af9d22e5b45348f600ce7d4b650a43e0 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 3 Mar 2021 10:03:28 +0000 Subject: [PATCH 05/26] Slight UI adjustments 2 Signed-off-by: Andrew Thornton --- options/locale/locale_en-US.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 9f58467371788..f8c519fe2883a 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -534,9 +534,9 @@ ssh_key_been_used = This SSH key has already been added to the server. ssh_key_name_used = An SSH key with same name already exists on your account. ssh_principal_been_used = This principal has already been added to the server. gpg_key_id_used = A public GPG key with same ID already exists. -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. +gpg_no_key_email_found = This GPG key does not match any activated email address associated with your account. It may still be added if you sign the provided token. gpg_key_matched_identities = Matched Identities: -gpg_key_matched_identities_long=The embedded identities in this key match the following activated email addresses for this user and commits with matching these email addresses can be verified with this key. +gpg_key_matched_identities_long=The embedded identities in this key match the following activated email addresses for this user and commits matching these email addresses can be verified with this key. gpg_key_verified=Verified Key gpg_key_verified_long=Key has been verified with a token and can be used to verify commits matching any activated email addresses for this user in addition to any matched identities for this key. gpg_key_verify=Verify From 66f6e7df40d4f853a922c328a3af610348c1598d Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 3 Mar 2021 11:23:01 +0000 Subject: [PATCH 06/26] Simplify signature verification slightly Signed-off-by: Andrew Thornton --- models/gpg_key.go | 111 ++++++++++++++++++++++++++++------------------ 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/models/gpg_key.go b/models/gpg_key.go index a287a88b93c2d..b08e283b82485 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -233,37 +233,49 @@ func VerifyGPGKey(ownerID, keyID int64, token, signature string) (string, error) return "", ErrGPGKeyNotExist{keyID} } - pkey, err := base64DecPubKey(key.Content) + sig, err := extractSignature(signature) if err != nil { - return "", err + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } } - ent := &openpgp.Entity{ - PrimaryKey: pkey, - Identities: map[string]*openpgp.Identity{ - "": {Name: "", SelfSignature: &packet.Signature{}}, - }, + signer, err := hashAndVerifyWithSubKeys(sig, token, key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } } + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key) - ekeys := openpgp.EntityList([]*openpgp.Entity{ - ent, - }) - - signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) - if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } } - if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } } - if err != nil { + + if signer == nil { log.Error("Unable to validate token signature. Error: %v", err) return "", ErrGPGInvalidTokenSignature{ - ID: ekeys[0].PrimaryKey.KeyIdString(), - Wrapped: err, + ID: key.KeyID, } } - if signer.PrimaryKey.KeyIdString() != key.KeyID { + + if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { return "", ErrGPGKeyNotExist{keyID} } @@ -554,11 +566,38 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { return pkey.VerifySignature(h, s) } -func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { +func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { //Generating hash of commit hash, err := populateHash(sig.Hash, []byte(payload)) - if err != nil { //Skipping failed to generate hash + if err != nil { //Skipping as failed to generate hash log.Error("PopulateHash: %v", err) + return nil, err + } + // We will ignore errors in verification as they don't need to be propagated up + err = verifySign(sig, hash, k) + if err != nil { + return nil, nil + } + return k, nil +} + +func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { + verified, err := hashAndVerify(sig, payload, k) + if err != nil || verified != nil { + return verified, err + } + for _, sk := range k.SubsKey { + verified, err := hashAndVerify(sig, payload, sk) + if err != nil || verified != nil { + return verified, err + } + } + return nil, nil +} + +func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { + key, err := hashAndVerifyWithSubKeys(sig, payload, k) + if err != nil { //Skipping failed to generate hash return &CommitVerification{ CommittingUser: committer, Verified: false, @@ -566,35 +605,19 @@ func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, } } - if err := verifySign(sig, hash, k); err == nil { + if key != nil { return &CommitVerification{ //Everything is ok CommittingUser: committer, Verified: true, - Reason: fmt.Sprintf("%s / %s", signer.Name, k.KeyID), + Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), SigningUser: signer, - SigningKey: k, + SigningKey: key, SigningEmail: email, } } return nil } -func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { - commitVerification := hashAndVerify(sig, payload, k, committer, signer, email) - if commitVerification != nil { - return commitVerification - } - - //And test also SubsKey - for _, sk := range k.SubsKey { - commitVerification := hashAndVerify(sig, payload, sk, committer, signer, email) - if commitVerification != nil { - return commitVerification - } - } - return nil -} - func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { uid := int64(0) var userEmails []*EmailAddress @@ -671,7 +694,7 @@ func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *Use } } } - commitVerification := hashAndVerifyWithSubKeys(sig, payload, key, committer, signer, email) + commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) if commitVerification != nil { return commitVerification } @@ -782,7 +805,7 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification { continue //Skip this key } - commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email) + commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) if commitVerification != nil { return commitVerification } @@ -880,7 +903,7 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, KeyID: subKey.PublicKey.KeyIdString(), }) } - if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{ + if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &User{ Name: gpgSettings.Name, Email: gpgSettings.Email, }, gpgSettings.Email); commitVerification != nil { From 3d145ec9474f6eaf70a6522f4a518b41495627fc Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 3 Mar 2021 16:30:06 +0000 Subject: [PATCH 07/26] fix postgres test Signed-off-by: Andrew Thornton --- integrations/api_gpg_keys_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/integrations/api_gpg_keys_test.go b/integrations/api_gpg_keys_test.go index e664c3c256553..a3895477c2bd8 100644 --- a/integrations/api_gpg_keys_test.go +++ b/integrations/api_gpg_keys_test.go @@ -77,6 +77,9 @@ func TestGPGKeys(t *testing.T) { DecodeJSON(t, resp, &keys) primaryKey1 := keys[0] //Primary key 1 + if primaryKey1.KeyID != "38EA3BCED732982C" { + primaryKey1 = keys[1] + } assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID) assert.EqualValues(t, 1, len(primaryKey1.Emails)) assert.EqualValues(t, "user2@example.com", primaryKey1.Emails[0].Email) @@ -87,6 +90,9 @@ func TestGPGKeys(t *testing.T) { assert.EqualValues(t, 0, len(subKey.Emails)) primaryKey2 := keys[1] //Primary key 2 + if primaryKey2.KeyID != "FABF39739FE1E927" { + primaryKey2 = keys[0] + } assert.EqualValues(t, "FABF39739FE1E927", primaryKey2.KeyID) assert.EqualValues(t, 1, len(primaryKey2.Emails)) assert.EqualValues(t, "user21@example.com", primaryKey2.Emails[0].Email) From 3105706d262f292e6eef1194149395aa251b6261 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Wed, 3 Mar 2021 17:57:37 +0000 Subject: [PATCH 08/26] add api routes Signed-off-by: Andrew Thornton --- models/gpg_key.go | 10 ++-- modules/convert/convert.go | 2 + modules/forms/user_form.go | 2 +- modules/structs/user_gpgkey.go | 10 ++++ routers/api/v1/api.go | 3 ++ routers/api/v1/user/gpg_key.go | 70 ++++++++++++++++++++++++++- routers/user/setting/keys.go | 18 +++++-- templates/swagger/v1_json.tmpl | 50 +++++++++++++++++++ templates/user/settings/keys_gpg.tmpl | 10 ++-- 9 files changed, 158 insertions(+), 17 deletions(-) diff --git a/models/gpg_key.go b/models/gpg_key.go index b08e283b82485..aac5087cef011 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -217,7 +217,7 @@ func AddGPGKey(ownerID int64, content, token, signature string) ([]*GPGKey, erro } // VerifyGPGKey marks a GPG key as verified -func VerifyGPGKey(ownerID, keyID int64, token, signature string) (string, error) { +func VerifyGPGKey(ownerID int64, keyID, token, signature string) (string, error) { sess := x.NewSession() defer sess.Close() if err := sess.Begin(); err != nil { @@ -226,11 +226,11 @@ func VerifyGPGKey(ownerID, keyID int64, token, signature string) (string, error) key := new(GPGKey) - has, err := sess.ID(keyID).Get(key) + has, err := sess.Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key) if err != nil { return "", err - } else if !has || key.OwnerID != ownerID { - return "", ErrGPGKeyNotExist{keyID} + } else if !has { + return "", ErrGPGKeyNotExist{} } sig, err := extractSignature(signature) @@ -276,7 +276,7 @@ func VerifyGPGKey(ownerID, keyID int64, token, signature string) (string, error) } if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { - return "", ErrGPGKeyNotExist{keyID} + return "", ErrGPGKeyNotExist{} } key.Verified = true diff --git a/modules/convert/convert.go b/modules/convert/convert.go index 109931dbc3433..91c1e9b51e6c4 100644 --- a/modules/convert/convert.go +++ b/modules/convert/convert.go @@ -189,6 +189,7 @@ func ToGPGKey(key *models.GPGKey) *api.GPGKey { CanEncryptComms: k.CanEncryptComms, CanEncryptStorage: k.CanEncryptStorage, CanCertify: k.CanSign, + Verified: k.Verified, } } emails := make([]*api.GPGKeyEmail, len(key.Emails)) @@ -208,6 +209,7 @@ func ToGPGKey(key *models.GPGKey) *api.GPGKey { CanEncryptComms: key.CanEncryptComms, CanEncryptStorage: key.CanEncryptStorage, CanCertify: key.CanSign, + Verified: key.Verified, } } diff --git a/modules/forms/user_form.go b/modules/forms/user_form.go index f36a5e71e824e..24990e9e2ece8 100644 --- a/modules/forms/user_form.go +++ b/modules/forms/user_form.go @@ -325,7 +325,7 @@ type AddKeyForm struct { Title string `binding:"Required;MaxSize(50)"` Content string `binding:"Required"` Signature string `binding:"OmitEmpty"` - Key int64 `binding:"OmitEmpty"` + KeyID string `binding:"OmitEmpty"` IsWritable bool } diff --git a/modules/structs/user_gpgkey.go b/modules/structs/user_gpgkey.go index 1a4894f74dcae..a2ebf7df93b52 100644 --- a/modules/structs/user_gpgkey.go +++ b/modules/structs/user_gpgkey.go @@ -20,6 +20,7 @@ type GPGKey struct { CanEncryptComms bool `json:"can_encrypt_comms"` CanEncryptStorage bool `json:"can_encrypt_storage"` CanCertify bool `json:"can_certify"` + Verified bool `json:"verified"` // swagger:strfmt date-time Created time.Time `json:"created_at,omitempty"` // swagger:strfmt date-time @@ -42,3 +43,12 @@ type CreateGPGKeyOption struct { ArmoredKey string `json:"armored_public_key" binding:"Required"` Signature string `json:"armored_signature,omitempty"` } + +// VerifyGPGKeyOption options verifies user GPG key +type VerifyGPGKeyOption struct { + // An Signature for a GPG key token + // + // required: true + KeyID string `json:"key_id" binding:"Required"` + Signature string `json:"armored_signature" binding:"Required"` +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index a8499e0ee8f6a..c72f259fbaff2 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -673,6 +673,9 @@ func Routes() *web.Route { Delete(user.DeleteGPGKey) }) + m.Get("/gpg_key_token", user.GetVerificationToken) + m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey) + m.Combo("/repos").Get(user.ListMyRepos). Post(bind(api.CreateRepoOption{}), repo.Create) diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 1e88c8fc8d136..5a35ae7d4b08e 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -123,9 +123,13 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { - 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)) + token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) keys, err := models.AddGPGKey(uid, form.ArmoredKey, token, form.Signature) + if err != nil && models.IsErrGPGInvalidTokenSignature(err) { + keys, err = models.AddGPGKey(uid, form.ArmoredKey, lastToken, form.Signature) + } if err != nil { HandleAddGPGKeyError(ctx, err, token) return @@ -133,6 +137,70 @@ func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0])) } +// GetVerificationToken returns the current token to be signed for this user +func GetVerificationToken(ctx *context.APIContext) { + // swagger:operation GET /user/gpg_key_token user getVerificationToken + // --- + // summary: Get a Token to verify + // produces: + // - text/plain + // parameters: + // responses: + // "200": + // "$ref": "#/responses/string" + // "404": + // "$ref": "#/responses/notFound" + + token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + ctx.PlainText(http.StatusOK, []byte(token)) +} + +// VerifyUserGPGKey creates new GPG key to given user by ID. +func VerifyUserGPGKey(ctx *context.APIContext) { + // swagger:operation POST /user/gpg_key_verify user userVerifyGPGKey + // --- + // summary: Verify a GPG key + // consumes: + // - application/json + // produces: + // - application/json + // responses: + // "201": + // "$ref": "#/responses/GPGKey" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + form := web.GetForm(ctx).(*api.VerifyGPGKeyOption) + token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + + _, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature) + if err != nil && models.IsErrGPGInvalidTokenSignature(err) { + _, err = models.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature) + } + + if err != nil { + if models.IsErrGPGInvalidTokenSignature(err) { + 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)) + return + } + ctx.Error(http.StatusInternalServerError, "VerifyUserGPGKey", err) + } + + key, err := models.GetGPGKeysByKeyID(form.KeyID) + if err != nil { + if models.IsErrGPGKeyNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetGPGKeysByKeyID", err) + } + return + } + ctx.JSON(http.StatusOK, convert.ToGPGKey(key[0])) +} + // swagger:parameters userCurrentPostGPGKey type swaggerUserCurrentPostGPGKey struct { // in:body diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go index 51ebc602ce287..8816ac8a5dfba 100644 --- a/routers/user/setting/keys.go +++ b/routers/user/setting/keys.go @@ -77,9 +77,13 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": - 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)) + token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) keys, err := models.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature) + if err != nil && models.IsErrGPGInvalidTokenSignature(err) { + keys, err = models.AddGPGKey(ctx.User.ID, form.Content, lastToken, form.Signature) + } if err != nil { ctx.Data["HasGPGError"] = true switch { @@ -121,14 +125,18 @@ func KeysPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "verify_gpg": 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)) + lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) - keyID, err := models.VerifyGPGKey(ctx.User.ID, form.Key, token, form.Signature) + keyID, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature) + if err != nil && models.IsErrGPGInvalidTokenSignature(err) { + keyID, err = models.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature) + } if err != nil { ctx.Data["HasGPGVerifyError"] = true switch { case models.IsErrGPGInvalidTokenSignature(err): loadKeysData(ctx) - ctx.Data["VerifyingID"] = form.Key + ctx.Data["VerifyingID"] = form.KeyID ctx.Data["Err_Signature"] = true ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form) @@ -246,7 +254,7 @@ func loadKeysData(ctx *context.Context) { return } ctx.Data["GPGKeys"] = gpgkeys - 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)) + tokenToSign := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) // generate a new aes cipher using the csrfToken ctx.Data["TokenToSign"] = tokenToSign @@ -258,5 +266,5 @@ func loadKeysData(ctx *context.Context) { } ctx.Data["Principals"] = principals - ctx.Data["VerifyingID"] = ctx.QueryInt64("verify_gpg") + ctx.Data["VerifyingID"] = ctx.Query("verify_gpg") } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 1ea0d895d2991..8f74a69042960 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -10355,6 +10355,52 @@ } } }, + "/user/gpg_key_token": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "user" + ], + "summary": "Get a Token to verify", + "operationId": "getVerificationToken", + "responses": { + "200": { + "$ref": "#/responses/string" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, + "/user/gpg_key_verify": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "user" + ], + "summary": "Verify a GPG key", + "operationId": "userVerifyGPGKey", + "responses": { + "201": { + "$ref": "#/responses/GPGKey" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/user/gpg_keys": { "get": { "produces": [ @@ -14026,6 +14072,10 @@ "$ref": "#/definitions/GPGKey" }, "x-go-name": "SubsKey" + }, + "verified": { + "type": "boolean", + "x-go-name": "Verified" } }, "x-go-package": "code.gitea.io/gitea/modules/structs" diff --git a/templates/user/settings/keys_gpg.tmpl b/templates/user/settings/keys_gpg.tmpl index 2a82a9797844b..cb4c9cc4d9a90 100644 --- a/templates/user/settings/keys_gpg.tmpl +++ b/templates/user/settings/keys_gpg.tmpl @@ -15,8 +15,8 @@ - {{if and (not .Verified) (ne $.VerifyingID .ID)}} - {{$.i18n.Tr "settings.gpg_key_verify"}} + {{if and (not .Verified) (ne $.VerifyingID .KeyID)}} + {{$.i18n.Tr "settings.gpg_key_verify"}} {{end}}
@@ -40,14 +40,14 @@
- {{if and (not .Verified) (eq $.VerifyingID .ID)}} + {{if and (not .Verified) (eq $.VerifyingID .KeyID)}}

{{$.i18n.Tr "settings.gpg_token_required"}}

{{$.CsrfTokenHtml}} - - + +
From c4529b8037dd6c42ff5e825b92433b056f851854 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 20 Mar 2021 12:09:02 +0000 Subject: [PATCH 09/26] prepare for merge --- models/migrations/{v175.go => v178.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v175.go => v178.go} (100%) diff --git a/models/migrations/v175.go b/models/migrations/v178.go similarity index 100% rename from models/migrations/v175.go rename to models/migrations/v178.go From e3a2770cfce07e5843d0a5cca866185552c24f21 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 20 Mar 2021 12:37:05 +0000 Subject: [PATCH 10/26] as per @6543 Signed-off-by: Andrew Thornton --- integrations/api_gpg_keys_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/integrations/api_gpg_keys_test.go b/integrations/api_gpg_keys_test.go index a3895477c2bd8..c69d60535e4a2 100644 --- a/integrations/api_gpg_keys_test.go +++ b/integrations/api_gpg_keys_test.go @@ -75,6 +75,7 @@ func TestGPGKeys(t *testing.T) { req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) //GET all keys resp := session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &keys) + assert.Len(t, keys, 2) primaryKey1 := keys[0] //Primary key 1 if primaryKey1.KeyID != "38EA3BCED732982C" { From 164daeba0027e1cc1cc0139f8cc34592c7ae176d Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 6 Jun 2021 13:34:30 +0100 Subject: [PATCH 11/26] prepare for update Signed-off-by: Andrew Thornton --- models/migrations/{v180.go => v181.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v180.go => v181.go} (100%) diff --git a/models/migrations/v180.go b/models/migrations/v181.go similarity index 100% rename from models/migrations/v180.go rename to models/migrations/v181.go From c4d94efa8b0804b94bcdca6ad1802ad03fb6b054 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 14 Jun 2021 18:15:46 +0100 Subject: [PATCH 12/26] pre-merge Signed-off-by: Andrew Thornton --- models/migrations/{v181.go => v183.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v181.go => v183.go} (100%) diff --git a/models/migrations/v181.go b/models/migrations/v183.go similarity index 100% rename from models/migrations/v181.go rename to models/migrations/v183.go From 47d9cad4eacb07c352e0c6874a38f5eb0d0c6378 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Mon, 14 Jun 2021 18:25:45 +0100 Subject: [PATCH 13/26] prepare merge Signed-off-by: Andrew Thornton --- models/migrations/{v183.go => v184.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v183.go => v184.go} (100%) diff --git a/models/migrations/v183.go b/models/migrations/v184.go similarity index 100% rename from models/migrations/v183.go rename to models/migrations/v184.go From cee8a8c30d1c39e3cde8c5498b4118cbe9911da7 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Tue, 15 Jun 2021 20:16:19 +0100 Subject: [PATCH 14/26] handle swapped primarykeys Signed-off-by: Andrew Thornton --- integrations/api_gpg_keys_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integrations/api_gpg_keys_test.go b/integrations/api_gpg_keys_test.go index b78291d16f4e6..3296250bab170 100644 --- a/integrations/api_gpg_keys_test.go +++ b/integrations/api_gpg_keys_test.go @@ -76,9 +76,9 @@ func TestGPGKeys(t *testing.T) { DecodeJSON(t, resp, &keys) assert.Len(t, keys, 2) - primaryKey1 := keys[0] //Primary key 1 + primaryKey1, primaryKey2 := keys[0], keys[1] //Primary key 1 if primaryKey1.KeyID != "38EA3BCED732982C" { - primaryKey1 = keys[1] + primaryKey1, primaryKey2 = keys[1], keys[0] } assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID) assert.Len(t, primaryKey1.Emails, 1) @@ -89,7 +89,6 @@ func TestGPGKeys(t *testing.T) { assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID) assert.Empty(t, subKey.Emails) - primaryKey2 := keys[1] //Primary key 2 assert.EqualValues(t, "3CEF46EF40BEFC3E", primaryKey2.KeyID) assert.Len(t, primaryKey2.Emails, 1) assert.EqualValues(t, "user2-2@example.com", primaryKey2.Emails[0].Email) From 34d2ac77e32d15894885fd7820354be434a56cbc Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 17 Jun 2021 08:49:55 +0100 Subject: [PATCH 15/26] prepare to merge Signed-off-by: Andrew Thornton --- models/migrations/{v184.go => v185.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v184.go => v185.go} (100%) diff --git a/models/migrations/v184.go b/models/migrations/v185.go similarity index 100% rename from models/migrations/v184.go rename to models/migrations/v185.go From eb69eff6bbcd4aeb189e25729cdaa1456ee799a5 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Thu, 24 Jun 2021 19:54:59 +0100 Subject: [PATCH 16/26] prepare merge Signed-off-by: Andrew Thornton --- models/migrations/{v185.go => v186.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v185.go => v186.go} (100%) diff --git a/models/migrations/v185.go b/models/migrations/v186.go similarity index 100% rename from models/migrations/v185.go rename to models/migrations/v186.go From 7609edd718a9d738d6da8198cb9d6fc2167e09f6 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Fri, 25 Jun 2021 18:53:27 +0100 Subject: [PATCH 17/26] pre-merge Signed-off-by: Andrew Thornton --- models/migrations/{v186.go => v187.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename models/migrations/{v186.go => v187.go} (100%) diff --git a/models/migrations/v186.go b/models/migrations/v187.go similarity index 100% rename from models/migrations/v186.go rename to models/migrations/v187.go From 4e5c668adc172923a03083c88eb84e02758609a3 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sat, 26 Jun 2021 01:35:20 +0100 Subject: [PATCH 18/26] Verify the no-reply address for verified keys Signed-off-by: Andrew Thornton --- models/gpg_key.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/models/gpg_key.go b/models/gpg_key.go index d8e99529cf2a1..03de5995036ac 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -621,6 +621,7 @@ func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload s func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { uid := int64(0) var userEmails []*EmailAddress + var user *User for _, key := range keys { for _, e := range key.Emails { if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { @@ -631,12 +632,17 @@ func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { if uid != key.OwnerID { userEmails, _ = GetEmailAddresses(key.OwnerID) uid = key.OwnerID + user = &User{ID: uid} + _, _ = GetUser(user) } for _, e := range userEmails { if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { return true, e.Email } } + if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) { + return true, user.GetEmail() + } } } return false, email From 18df5554d94836c4625c77d0f1c0bb0fa56e4f44 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 27 Jun 2021 16:22:23 +0100 Subject: [PATCH 19/26] Only add email addresses that are activated to keys Signed-off-by: Andrew Thornton --- models/gpg_key.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/gpg_key.go b/models/gpg_key.go index 03de5995036ac..fc1a549e1f37f 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -400,7 +400,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity, verified bool) (*GPGKey, erro } email := strings.ToLower(strings.TrimSpace(ident.UserId.Email)) for _, e := range userEmails { - if e.LowerEmail == email { + if e.IsActivated && e.LowerEmail == email { emails = append(emails, e) break } From 020d5f77c803a176388f60f6e14b7596291f4b48 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 27 Jun 2021 16:23:46 +0100 Subject: [PATCH 20/26] fix committer shortcut properly Signed-off-by: Andrew Thornton --- models/gpg_key.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/models/gpg_key.go b/models/gpg_key.go index fc1a549e1f37f..17098825b8f35 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -796,15 +796,30 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification { } } + committerEmailAddresses, _ := GetEmailAddresses(committer.ID) + activated := false + for _, e := range committerEmailAddresses { + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { + activated = true + break + } + } + for _, k := range keys { // Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate canValidate := false email := "" - for _, e := range k.Emails { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - canValidate = true - email = e.Email - break + if k.Verified && activated { + canValidate = true + email = c.Committer.Email + } + if !canValidate { + for _, e := range k.Emails { + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { + canValidate = true + email = e.Email + break + } } } if !canValidate { From 6e2a15a522228218265f11c9f131f6d569a4ad67 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 27 Jun 2021 17:12:22 +0100 Subject: [PATCH 21/26] Restructure gpg_keys.go Signed-off-by: Andrew Thornton --- models/gpg_key.go | 789 +------------------------- models/gpg_key_add.go | 124 ++++ models/gpg_key_commit_verification.go | 519 +++++++++++++++++ models/gpg_key_common.go | 137 +++++ models/gpg_key_import.go | 38 ++ models/gpg_key_verify.go | 97 ++++ 6 files changed, 922 insertions(+), 782 deletions(-) create mode 100644 models/gpg_key_add.go create mode 100644 models/gpg_key_commit_verification.go create mode 100644 models/gpg_key_common.go create mode 100644 models/gpg_key_import.go create mode 100644 models/gpg_key_verify.go diff --git a/models/gpg_key.go b/models/gpg_key.go index 17098825b8f35..74ffb82a545b5 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -5,27 +5,25 @@ package models import ( - "bytes" - "container/list" - "crypto" - "encoding/base64" "fmt" - "hash" - "io" "strings" "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" "github.com/keybase/go-crypto/openpgp/packet" "xorm.io/xorm" ) +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ + // GPGKey represents a GPG key. type GPGKey struct { ID int64 `xorm:"pk autoincr"` @@ -45,12 +43,6 @@ 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 = timeutil.TimeStampNow() @@ -97,229 +89,6 @@ func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) { return keys, x.Where("key_id=?", keyID).Find(&keys) } -// 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.EntityList, error) { - list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) - if err != nil { - return nil, ErrGPGKeyParsing{err} - } - return list, nil -} - -// 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 := addGPGSubKey(e, subkey); err != nil { - return err - } - } - return nil -} - -// AddGPGKey adds new public key to database. -func AddGPGKey(ownerID int64, content, token, signature string) ([]*GPGKey, error) { - ekeys, err := checkArmoredGPGKeyString(content) - if err != nil { - return nil, err - } - - sess := x.NewSession() - defer sess.Close() - if err = sess.Begin(); err != nil { - return nil, err - } - keys := make([]*GPGKey, 0, len(ekeys)) - - verified := false - // Handle provided signature - if signature != "" { - signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) - if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) - } - if err != nil { - signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) - } - if err != nil { - log.Error("Unable to validate token signature. Error: %v", err) - return nil, ErrGPGInvalidTokenSignature{ - ID: ekeys[0].PrimaryKey.KeyIdString(), - Wrapped: err, - } - } - ekeys = []*openpgp.Entity{signer} - verified = true - } - - for _, ekey := range ekeys { - // Key ID cannot be duplicated. - has, err := sess.Where("key_id=?", ekey.PrimaryKey.KeyIdString()). - Get(new(GPGKey)) - if err != nil { - return nil, err - } else if has { - return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()} - } - - // Get DB session - - key, err := parseGPGKey(ownerID, ekey, verified) - if err != nil { - return nil, err - } - - if err = addGPGKey(sess, key, content); err != nil { - return nil, err - } - keys = append(keys, key) - } - return keys, sess.Commit() -} - -// VerifyGPGKey marks a GPG key as verified -func VerifyGPGKey(ownerID int64, keyID, token, signature string) (string, error) { - sess := x.NewSession() - defer sess.Close() - if err := sess.Begin(); err != nil { - return "", err - } - - key := new(GPGKey) - - has, err := sess.Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key) - if err != nil { - return "", err - } else if !has { - return "", ErrGPGKeyNotExist{} - } - - sig, err := extractSignature(signature) - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, - } - } - - signer, err := hashAndVerifyWithSubKeys(sig, token, key) - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, - } - } - if signer == nil { - signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key) - - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, - } - } - } - if signer == nil { - signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key) - if err != nil { - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - Wrapped: err, - } - } - } - - if signer == nil { - log.Error("Unable to validate token signature. Error: %v", err) - return "", ErrGPGInvalidTokenSignature{ - ID: key.KeyID, - } - } - - if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { - return "", ErrGPGKeyNotExist{} - } - - key.Verified = true - if _, err := sess.ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil { - return "", err - } - - if err := sess.Commit(); err != nil { - return "", err - } - - return key.KeyID, nil -} - -// base64EncPubKey encode public key content to base 64 -func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { - var w bytes.Buffer - err := pubkey.Serialize(&w) - if err != nil { - return "", err - } - 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) @@ -353,25 +122,6 @@ func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, e }, nil } -// getExpiryTime extract the expire time of primary key based on sig -func getExpiryTime(e *openpgp.Entity) time.Time { - expiry := time.Time{} - // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 - var selfSig *packet.Signature - for _, ident := range e.Identities { - if selfSig == nil { - selfSig = ident.SelfSignature - } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { - selfSig = ident.SelfSignature - break - } - } - if selfSig.KeyLifetimeSecs != nil { - expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) - } - return expiry -} - // parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature) func parseGPGKey(ownerID int64, e *openpgp.Entity, verified bool) (*GPGKey, error) { pubkey := e.PrimaryKey @@ -480,144 +230,6 @@ func DeleteGPGKey(doer *User, id int64) (err error) { return sess.Commit() } -// CommitVerification represents a commit validation of signature -type CommitVerification struct { - Verified bool - Warning bool - Reason string - SigningUser *User - CommittingUser *User - SigningEmail string - SigningKey *GPGKey - TrustStatus string -} - -// SignCommit represents a commit with validation of signature. -type SignCommit struct { - Verification *CommitVerification - *UserCommit -} - -const ( - // BadSignature is used as the reason when the signature has a KeyID that is in the db - // but no key that has that ID verifies the signature. This is a suspicious failure. - BadSignature = "gpg.error.probable_bad_signature" - // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the - // default Key but is not verified by the default key. This is a suspicious failure. - BadDefaultSignature = "gpg.error.probable_bad_default_signature" - // NoKeyFound is used as the reason when no key can be found to verify the signature. - NoKeyFound = "gpg.error.no_gpg_keys_found" -) - -func readerFromBase64(s string) (io.Reader, error) { - bs, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, err - } - return bytes.NewBuffer(bs), nil -} - -func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) { - h := hashFunc.New() - if _, err := h.Write(msg); err != nil { - return nil, err - } - return h, nil -} - -// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17 -func readArmoredSign(r io.Reader) (body io.Reader, err error) { - block, err := armor.Decode(r) - if err != nil { - return - } - if block.Type != openpgp.SignatureType { - return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type) - } - return block.Body, nil -} - -func extractSignature(s string) (*packet.Signature, error) { - r, err := readArmoredSign(strings.NewReader(s)) - if err != nil { - return nil, fmt.Errorf("Failed to read signature armor") - } - p, err := packet.Read(r) - if err != nil { - return nil, fmt.Errorf("Failed to read signature packet") - } - sig, ok := p.(*packet.Signature) - if !ok { - return nil, fmt.Errorf("Packet is not a signature") - } - return sig, nil -} - -func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { - // Check if key can sign - if !k.CanSign { - return fmt.Errorf("key can not sign") - } - // Decode key - pkey, err := base64DecPubKey(k.Content) - if err != nil { - return err - } - return pkey.VerifySignature(h, s) -} - -func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { - // Generating hash of commit - hash, err := populateHash(sig.Hash, []byte(payload)) - if err != nil { // Skipping as failed to generate hash - log.Error("PopulateHash: %v", err) - return nil, err - } - // We will ignore errors in verification as they don't need to be propagated up - err = verifySign(sig, hash, k) - if err != nil { - return nil, nil - } - return k, nil -} - -func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { - verified, err := hashAndVerify(sig, payload, k) - if err != nil || verified != nil { - return verified, err - } - for _, sk := range k.SubsKey { - verified, err := hashAndVerify(sig, payload, sk) - if err != nil || verified != nil { - return verified, err - } - } - return nil, nil -} - -func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { - key, err := hashAndVerifyWithSubKeys(sig, payload, k) - if err != nil { // Skipping failed to generate hash - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - - if key != nil { - return &CommitVerification{ // Everything is ok - CommittingUser: committer, - Verified: true, - Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), - SigningUser: signer, - SigningKey: key, - SigningEmail: email, - } - } - return nil -} - func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { uid := int64(0) var userEmails []*EmailAddress @@ -647,390 +259,3 @@ func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { } return false, email } - -func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification { - if keyID == "" { - return nil - } - keys, err := GetGPGKeysByKeyID(keyID) - if err != nil { - log.Error("GetGPGKeysByKeyID: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - if len(keys) == 0 { - return nil - } - for _, key := range keys { - var primaryKeys []*GPGKey - if key.PrimaryKeyID != "" { - primaryKeys, err = GetGPGKeysByKeyID(key.PrimaryKeyID) - if err != nil { - log.Error("GetGPGKeysByKeyID: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - } - - activated, email := checkKeyEmails(email, append([]*GPGKey{key}, primaryKeys...)...) - if !activated { - continue - } - - signer := &User{ - Name: name, - Email: email, - } - if key.OwnerID != 0 { - owner, err := GetUserByID(key.OwnerID) - if err == nil { - signer = owner - } else if !IsErrUserNotExist(err) { - log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } - } - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) - if commitVerification != nil { - return commitVerification - } - } - // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Warning: true, - Reason: BadSignature, - } -} - -// ParseCommitWithSignature check if signature is good against keystore. -func ParseCommitWithSignature(c *git.Commit) *CommitVerification { - var committer *User - if c.Committer != nil { - var err error - // Find Committer account - committer, err = GetUserByEmail(c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not - if err != nil { // Skipping not user for commiter - committer = &User{ - Name: c.Committer.Name, - Email: c.Committer.Email, - } - // We can expect this to often be an ErrUserNotExist. in the case - // it is not, however, it is important to log it. - if !IsErrUserNotExist(err) { - log.Error("GetUserByEmail: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.no_committer_account", - } - } - - } - } - - // If no signature just report the committer - if c.Signature == nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, // Default value - Reason: "gpg.error.not_signed_commit", // Default value - } - } - - // Parsing signature - sig, err := extractSignature(c.Signature.Signature) - if err != nil { // Skipping failed to extract sign - log.Error("SignatureRead err: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.extract_sign", - } - } - - keyID := "" - if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { - keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) - } - if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { - keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) - } - defaultReason := NoKeyFound - - // First check if the sig has a keyID and if so just look at that - if commitVerification := hashAndVerifyForKeyID( - sig, - c.Signature.Payload, - committer, - keyID, - setting.AppName, - ""); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - - // Now try to associate the signature with the committer, if present - if committer.ID != 0 { - keys, err := ListGPGKeys(committer.ID, ListOptions{}) - if err != nil { // Skipping failed to get gpg keys of user - log.Error("ListGPGKeys: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.failed_retrieval_gpg_keys", - } - } - - committerEmailAddresses, _ := GetEmailAddresses(committer.ID) - activated := false - for _, e := range committerEmailAddresses { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - activated = true - break - } - } - - for _, k := range keys { - // Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate - canValidate := false - email := "" - if k.Verified && activated { - canValidate = true - email = c.Committer.Email - } - if !canValidate { - for _, e := range k.Emails { - if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { - canValidate = true - email = e.Email - break - } - } - } - if !canValidate { - continue // Skip this key - } - - commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) - if commitVerification != nil { - return commitVerification - } - } - } - - if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { - // OK we should try the default key - gpgSettings := git.GPGSettings{ - Sign: true, - KeyID: setting.Repository.Signing.SigningKey, - Name: setting.Repository.Signing.SigningName, - Email: setting.Repository.Signing.SigningEmail, - } - if err := gpgSettings.LoadPublicKeyContent(); err != nil { - log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) - } else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - } - - defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) - if err != nil { - log.Error("Error getting default public gpg key: %v", err) - } else if defaultGPGSettings == nil { - log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) - } else if defaultGPGSettings.Sign { - if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { - if commitVerification.Reason == BadSignature { - defaultReason = BadSignature - } else { - return commitVerification - } - } - } - - return &CommitVerification{ // Default at this stage - CommittingUser: committer, - Verified: false, - Warning: defaultReason != NoKeyFound, - Reason: defaultReason, - SigningKey: &GPGKey{ - KeyID: keyID, - }, - } -} - -func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification { - // First try to find the key in the db - if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { - return commitVerification - } - - // Otherwise we have to parse the key - ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) - if err != nil { - log.Error("Unable to get default signing key: %v", err) - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - for _, ekey := range ekeys { - pubkey := ekey.PrimaryKey - content, err := base64EncPubKey(pubkey) - if err != nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - k := &GPGKey{ - Content: content, - CanSign: pubkey.CanSign(), - KeyID: pubkey.KeyIdString(), - } - for _, subKey := range ekey.Subkeys { - content, err := base64EncPubKey(subKey.PublicKey) - if err != nil { - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Reason: "gpg.error.generate_hash", - } - } - k.SubsKey = append(k.SubsKey, &GPGKey{ - Content: content, - CanSign: subKey.PublicKey.CanSign(), - KeyID: subKey.PublicKey.KeyIdString(), - }) - } - if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &User{ - Name: gpgSettings.Name, - Email: gpgSettings.Email, - }, gpgSettings.Email); commitVerification != nil { - return commitVerification - } - if keyID == k.KeyID { - // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. - return &CommitVerification{ - CommittingUser: committer, - Verified: false, - Warning: true, - Reason: BadSignature, - } - } - } - return nil -} - -// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. -func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List { - var ( - newCommits = list.New() - e = oldCommits.Front() - ) - keyMap := map[string]bool{} - - for e != nil { - c := e.Value.(UserCommit) - signCommit := SignCommit{ - UserCommit: &c, - Verification: ParseCommitWithSignature(c.Commit), - } - - _ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap) - - newCommits.PushBack(signCommit) - e = e.Next() - } - return newCommits -} - -// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository -func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) { - if !verification.Verified { - return - } - - // There are several trust models in Gitea - trustModel := repository.GetTrustModel() - - // In the Committer trust model a signature is trusted if it matches the committer - // - it doesn't matter if they're a collaborator, the owner, Gitea or Github - // NB: This model is commit verification only - if trustModel == CommitterTrustModel { - // default to "unmatched" - verification.TrustStatus = "unmatched" - - // We can only verify against users in our database but the default key will match - // against by email if it is not in the db. - if (verification.SigningUser.ID != 0 && - verification.CommittingUser.ID == verification.SigningUser.ID) || - (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && - verification.SigningUser.Email == verification.CommittingUser.Email) { - verification.TrustStatus = "trusted" - } - return - } - - // Now we drop to the more nuanced trust models... - verification.TrustStatus = "trusted" - - if verification.SigningUser.ID == 0 { - // This commit is signed by the default key - but this key is not assigned to a user in the DB. - - // However in the CollaboratorCommitterTrustModel we cannot mark this as trusted - // unless the default key matches the email of a non-user. - if trustModel == CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || - verification.SigningUser.Email != verification.CommittingUser.Email) { - verification.TrustStatus = "untrusted" - } - return - } - - var isMember bool - if keyMap != nil { - var has bool - isMember, has = (*keyMap)[verification.SigningKey.KeyID] - if !has { - isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) - (*keyMap)[verification.SigningKey.KeyID] = isMember - } - } else { - isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) - } - - if !isMember { - verification.TrustStatus = "untrusted" - if verification.CommittingUser.ID != verification.SigningUser.ID { - // The committing user and the signing user are not the same - // This should be marked as questionable unless the signing user is a collaborator/team member etc. - verification.TrustStatus = "unmatched" - } - } else if trustModel == CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { - // The committing user and the signing user are not the same and our trustmodel states that they must match - verification.TrustStatus = "unmatched" - } - - return -} diff --git a/models/gpg_key_add.go b/models/gpg_key_add.go new file mode 100644 index 0000000000000..04ad7cb3002cc --- /dev/null +++ b/models/gpg_key_add.go @@ -0,0 +1,124 @@ +// Copyright 2021 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 models + +import ( + "strings" + + "code.gitea.io/gitea/modules/log" + "github.com/keybase/go-crypto/openpgp" +) + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// _____ .___ .___ +// / _ \ __| _/__| _/ +// / /_\ \ / __ |/ __ | +// / | \/ /_/ / /_/ | +// \____|__ /\____ \____ | +// \/ \/ \/ + +// This file contains functions relating to adding GPG Keys + +// 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 := addGPGSubKey(e, subkey); err != nil { + return err + } + } + return nil +} + +// AddGPGKey adds new public key to database. +func AddGPGKey(ownerID int64, content, token, signature string) ([]*GPGKey, error) { + ekeys, err := checkArmoredGPGKeyString(content) + if err != nil { + return nil, err + } + + sess := x.NewSession() + defer sess.Close() + if err = sess.Begin(); err != nil { + return nil, err + } + keys := make([]*GPGKey, 0, len(ekeys)) + + verified := false + // Handle provided signature + if signature != "" { + signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature)) + if err != nil { + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature)) + } + if err != nil { + signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature)) + } + if err != nil { + log.Error("Unable to validate token signature. Error: %v", err) + return nil, ErrGPGInvalidTokenSignature{ + ID: ekeys[0].PrimaryKey.KeyIdString(), + Wrapped: err, + } + } + ekeys = []*openpgp.Entity{signer} + verified = true + } + + for _, ekey := range ekeys { + // Key ID cannot be duplicated. + has, err := sess.Where("key_id=?", ekey.PrimaryKey.KeyIdString()). + Get(new(GPGKey)) + if err != nil { + return nil, err + } else if has { + return nil, ErrGPGKeyIDAlreadyUsed{ekey.PrimaryKey.KeyIdString()} + } + + // Get DB session + + key, err := parseGPGKey(ownerID, ekey, verified) + if err != nil { + return nil, err + } + + if err = addGPGKey(sess, key, content); err != nil { + return nil, err + } + keys = append(keys, key) + } + return keys, sess.Commit() +} diff --git a/models/gpg_key_commit_verification.go b/models/gpg_key_commit_verification.go new file mode 100644 index 0000000000000..17d22e58fa9cb --- /dev/null +++ b/models/gpg_key_commit_verification.go @@ -0,0 +1,519 @@ +// Copyright 2021 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 models + +import ( + "container/list" + "fmt" + "hash" + "strings" + + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + "github.com/keybase/go-crypto/openpgp/packet" +) + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// _________ .__ __ +// \_ ___ \ ____ _____ _____ |__|/ |_ +// / \ \/ / _ \ / \ / \| \ __\ +// \ \___( <_> ) Y Y \ Y Y \ || | +// \______ /\____/|__|_| /__|_| /__||__| +// \/ \/ \/ +// ____ ____ .__ _____.__ __ .__ +// \ \ / /___________|__|/ ____\__| ____ _____ _/ |_|__| ____ ____ +// \ Y // __ \_ __ \ \ __\| |/ ___\\__ \\ __\ |/ _ \ / \ +// \ /\ ___/| | \/ || | | \ \___ / __ \| | | ( <_> ) | \ +// \___/ \___ >__| |__||__| |__|\___ >____ /__| |__|\____/|___| / +// \/ \/ \/ \/ + +// This file provides functions relating commit verification + +// CommitVerification represents a commit validation of signature +type CommitVerification struct { + Verified bool + Warning bool + Reason string + SigningUser *User + CommittingUser *User + SigningEmail string + SigningKey *GPGKey + TrustStatus string +} + +// SignCommit represents a commit with validation of signature. +type SignCommit struct { + Verification *CommitVerification + *UserCommit +} + +const ( + // BadSignature is used as the reason when the signature has a KeyID that is in the db + // but no key that has that ID verifies the signature. This is a suspicious failure. + BadSignature = "gpg.error.probable_bad_signature" + // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the + // default Key but is not verified by the default key. This is a suspicious failure. + BadDefaultSignature = "gpg.error.probable_bad_default_signature" + // NoKeyFound is used as the reason when no key can be found to verify the signature. + NoKeyFound = "gpg.error.no_gpg_keys_found" +) + +// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys. +func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List { + var ( + newCommits = list.New() + e = oldCommits.Front() + ) + keyMap := map[string]bool{} + + for e != nil { + c := e.Value.(UserCommit) + signCommit := SignCommit{ + UserCommit: &c, + Verification: ParseCommitWithSignature(c.Commit), + } + + _ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap) + + newCommits.PushBack(signCommit) + e = e.Next() + } + return newCommits +} + +// ParseCommitWithSignature check if signature is good against keystore. +func ParseCommitWithSignature(c *git.Commit) *CommitVerification { + var committer *User + if c.Committer != nil { + var err error + // Find Committer account + committer, err = GetUserByEmail(c.Committer.Email) // This finds the user by primary email or activated email so commit will not be valid if email is not + if err != nil { // Skipping not user for commiter + committer = &User{ + Name: c.Committer.Name, + Email: c.Committer.Email, + } + // We can expect this to often be an ErrUserNotExist. in the case + // it is not, however, it is important to log it. + if !IsErrUserNotExist(err) { + log.Error("GetUserByEmail: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.no_committer_account", + } + } + + } + } + + // If no signature just report the committer + if c.Signature == nil { + return &CommitVerification{ + CommittingUser: committer, + Verified: false, // Default value + Reason: "gpg.error.not_signed_commit", // Default value + } + } + + // Parsing signature + sig, err := extractSignature(c.Signature.Signature) + if err != nil { // Skipping failed to extract sign + log.Error("SignatureRead err: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.extract_sign", + } + } + + keyID := "" + if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 { + keyID = fmt.Sprintf("%X", *sig.IssuerKeyId) + } + if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 { + keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20]) + } + defaultReason := NoKeyFound + + // First check if the sig has a keyID and if so just look at that + if commitVerification := hashAndVerifyForKeyID( + sig, + c.Signature.Payload, + committer, + keyID, + setting.AppName, + ""); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } + + // Now try to associate the signature with the committer, if present + if committer.ID != 0 { + keys, err := ListGPGKeys(committer.ID, ListOptions{}) + if err != nil { // Skipping failed to get gpg keys of user + log.Error("ListGPGKeys: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.failed_retrieval_gpg_keys", + } + } + + committerEmailAddresses, _ := GetEmailAddresses(committer.ID) + activated := false + for _, e := range committerEmailAddresses { + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { + activated = true + break + } + } + + for _, k := range keys { + // Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate + canValidate := false + email := "" + if k.Verified && activated { + canValidate = true + email = c.Committer.Email + } + if !canValidate { + for _, e := range k.Emails { + if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) { + canValidate = true + email = e.Email + break + } + } + } + if !canValidate { + continue // Skip this key + } + + commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, c.Signature.Payload, k, committer, committer, email) + if commitVerification != nil { + return commitVerification + } + } + } + + if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { + // OK we should try the default key + gpgSettings := git.GPGSettings{ + Sign: true, + KeyID: setting.Repository.Signing.SigningKey, + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) + } else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } + } + + defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) + if err != nil { + log.Error("Error getting default public gpg key: %v", err) + } else if defaultGPGSettings == nil { + log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) + } else if defaultGPGSettings.Sign { + if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil { + if commitVerification.Reason == BadSignature { + defaultReason = BadSignature + } else { + return commitVerification + } + } + } + + return &CommitVerification{ // Default at this stage + CommittingUser: committer, + Verified: false, + Warning: defaultReason != NoKeyFound, + Reason: defaultReason, + SigningKey: &GPGKey{ + KeyID: keyID, + }, + } +} + +func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification { + // First try to find the key in the db + if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil { + return commitVerification + } + + // Otherwise we have to parse the key + ekeys, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent) + if err != nil { + log.Error("Unable to get default signing key: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + for _, ekey := range ekeys { + pubkey := ekey.PrimaryKey + content, err := base64EncPubKey(pubkey) + if err != nil { + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + k := &GPGKey{ + Content: content, + CanSign: pubkey.CanSign(), + KeyID: pubkey.KeyIdString(), + } + for _, subKey := range ekey.Subkeys { + content, err := base64EncPubKey(subKey.PublicKey) + if err != nil { + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + k.SubsKey = append(k.SubsKey, &GPGKey{ + Content: content, + CanSign: subKey.PublicKey.CanSign(), + KeyID: subKey.PublicKey.KeyIdString(), + }) + } + if commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, k, committer, &User{ + Name: gpgSettings.Name, + Email: gpgSettings.Email, + }, gpgSettings.Email); commitVerification != nil { + return commitVerification + } + if keyID == k.KeyID { + // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match. + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Warning: true, + Reason: BadSignature, + } + } + } + return nil +} + +func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { + // Check if key can sign + if !k.CanSign { + return fmt.Errorf("key can not sign") + } + // Decode key + pkey, err := base64DecPubKey(k.Content) + if err != nil { + return err + } + return pkey.VerifySignature(h, s) +} + +func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { + // Generating hash of commit + hash, err := populateHash(sig.Hash, []byte(payload)) + if err != nil { // Skipping as failed to generate hash + log.Error("PopulateHash: %v", err) + return nil, err + } + // We will ignore errors in verification as they don't need to be propagated up + err = verifySign(sig, hash, k) + if err != nil { + return nil, nil + } + return k, nil +} + +func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey) (*GPGKey, error) { + verified, err := hashAndVerify(sig, payload, k) + if err != nil || verified != nil { + return verified, err + } + for _, sk := range k.SubsKey { + verified, err := hashAndVerify(sig, payload, sk) + if err != nil || verified != nil { + return verified, err + } + } + return nil, nil +} + +func hashAndVerifyWithSubKeysCommitVerification(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification { + key, err := hashAndVerifyWithSubKeys(sig, payload, k) + if err != nil { // Skipping failed to generate hash + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.generate_hash", + } + } + + if key != nil { + return &CommitVerification{ // Everything is ok + CommittingUser: committer, + Verified: true, + Reason: fmt.Sprintf("%s / %s", signer.Name, key.KeyID), + SigningUser: signer, + SigningKey: key, + SigningEmail: email, + } + } + return nil +} + +func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification { + if keyID == "" { + return nil + } + keys, err := GetGPGKeysByKeyID(keyID) + if err != nil { + log.Error("GetGPGKeysByKeyID: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.failed_retrieval_gpg_keys", + } + } + if len(keys) == 0 { + return nil + } + for _, key := range keys { + var primaryKeys []*GPGKey + if key.PrimaryKeyID != "" { + primaryKeys, err = GetGPGKeysByKeyID(key.PrimaryKeyID) + if err != nil { + log.Error("GetGPGKeysByKeyID: %v", err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.failed_retrieval_gpg_keys", + } + } + } + + activated, email := checkKeyEmails(email, append([]*GPGKey{key}, primaryKeys...)...) + if !activated { + continue + } + + signer := &User{ + Name: name, + Email: email, + } + if key.OwnerID != 0 { + owner, err := GetUserByID(key.OwnerID) + if err == nil { + signer = owner + } else if !IsErrUserNotExist(err) { + log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err) + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Reason: "gpg.error.no_committer_account", + } + } + } + commitVerification := hashAndVerifyWithSubKeysCommitVerification(sig, payload, key, committer, signer, email) + if commitVerification != nil { + return commitVerification + } + } + // This is a bad situation ... We have a key id that is in our database but the signature doesn't match. + return &CommitVerification{ + CommittingUser: committer, + Verified: false, + Warning: true, + Reason: BadSignature, + } +} + +// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository +func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) { + if !verification.Verified { + return + } + + // There are several trust models in Gitea + trustModel := repository.GetTrustModel() + + // In the Committer trust model a signature is trusted if it matches the committer + // - it doesn't matter if they're a collaborator, the owner, Gitea or Github + // NB: This model is commit verification only + if trustModel == CommitterTrustModel { + // default to "unmatched" + verification.TrustStatus = "unmatched" + + // We can only verify against users in our database but the default key will match + // against by email if it is not in the db. + if (verification.SigningUser.ID != 0 && + verification.CommittingUser.ID == verification.SigningUser.ID) || + (verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 && + verification.SigningUser.Email == verification.CommittingUser.Email) { + verification.TrustStatus = "trusted" + } + return + } + + // Now we drop to the more nuanced trust models... + verification.TrustStatus = "trusted" + + if verification.SigningUser.ID == 0 { + // This commit is signed by the default key - but this key is not assigned to a user in the DB. + + // However in the CollaboratorCommitterTrustModel we cannot mark this as trusted + // unless the default key matches the email of a non-user. + if trustModel == CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 || + verification.SigningUser.Email != verification.CommittingUser.Email) { + verification.TrustStatus = "untrusted" + } + return + } + + var isMember bool + if keyMap != nil { + var has bool + isMember, has = (*keyMap)[verification.SigningKey.KeyID] + if !has { + isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) + (*keyMap)[verification.SigningKey.KeyID] = isMember + } + } else { + isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID) + } + + if !isMember { + verification.TrustStatus = "untrusted" + if verification.CommittingUser.ID != verification.SigningUser.ID { + // The committing user and the signing user are not the same + // This should be marked as questionable unless the signing user is a collaborator/team member etc. + verification.TrustStatus = "unmatched" + } + } else if trustModel == CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID { + // The committing user and the signing user are not the same and our trustmodel states that they must match + verification.TrustStatus = "unmatched" + } + + return +} diff --git a/models/gpg_key_common.go b/models/gpg_key_common.go new file mode 100644 index 0000000000000..72803625eeb87 --- /dev/null +++ b/models/gpg_key_common.go @@ -0,0 +1,137 @@ +// Copyright 2021 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 models + +import ( + "bytes" + "crypto" + "encoding/base64" + "fmt" + "hash" + "io" + "strings" + "time" + + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/armor" + "github.com/keybase/go-crypto/openpgp/packet" +) + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// _________ +// \_ ___ \ ____ _____ _____ ____ ____ +// / \ \/ / _ \ / \ / \ / _ \ / \ +// \ \___( <_> ) Y Y \ Y Y ( <_> ) | \ +// \______ /\____/|__|_| /__|_| /\____/|___| / +// \/ \/ \/ \/ + +// This file provides common functions relating to GPG Keys + +// 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.EntityList, error) { + list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) + if err != nil { + return nil, ErrGPGKeyParsing{err} + } + return list, nil +} + +// base64EncPubKey encode public key content to base 64 +func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { + var w bytes.Buffer + err := pubkey.Serialize(&w) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(w.Bytes()), nil +} + +func readerFromBase64(s string) (io.Reader, error) { + bs, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + return bytes.NewBuffer(bs), 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 +} + +// getExpiryTime extract the expire time of primary key based on sig +func getExpiryTime(e *openpgp.Entity) time.Time { + expiry := time.Time{} + // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 + var selfSig *packet.Signature + for _, ident := range e.Identities { + if selfSig == nil { + selfSig = ident.SelfSignature + } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + selfSig = ident.SelfSignature + break + } + } + if selfSig.KeyLifetimeSecs != nil { + expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) + } + return expiry +} + +func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) { + h := hashFunc.New() + if _, err := h.Write(msg); err != nil { + return nil, err + } + return h, nil +} + +// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17 +func readArmoredSign(r io.Reader) (body io.Reader, err error) { + block, err := armor.Decode(r) + if err != nil { + return + } + if block.Type != openpgp.SignatureType { + return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type) + } + return block.Body, nil +} + +func extractSignature(s string) (*packet.Signature, error) { + r, err := readArmoredSign(strings.NewReader(s)) + if err != nil { + return nil, fmt.Errorf("Failed to read signature armor") + } + p, err := packet.Read(r) + if err != nil { + return nil, fmt.Errorf("Failed to read signature packet") + } + sig, ok := p.(*packet.Signature) + if !ok { + return nil, fmt.Errorf("Packet is not a signature") + } + return sig, nil +} diff --git a/models/gpg_key_import.go b/models/gpg_key_import.go new file mode 100644 index 0000000000000..bd1d530eca26b --- /dev/null +++ b/models/gpg_key_import.go @@ -0,0 +1,38 @@ +// Copyright 2021 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 models + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// .___ __ +// | | _____ ______ ____________/ |_ +// | |/ \\____ \ / _ \_ __ \ __\ +// | | Y Y \ |_> > <_> ) | \/| | +// |___|__|_| / __/ \____/|__| |__| +// \/|__| + +// This file contains functions related to the original import of a key + +// GPGKeyImport the original import of key +type GPGKeyImport struct { + KeyID string `xorm:"pk CHAR(16) NOT NULL"` + Content string `xorm:"TEXT NOT NULL"` +} + +// 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 +} diff --git a/models/gpg_key_verify.go b/models/gpg_key_verify.go new file mode 100644 index 0000000000000..83743c74ed16e --- /dev/null +++ b/models/gpg_key_verify.go @@ -0,0 +1,97 @@ +// Copyright 2021 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 models + +import "code.gitea.io/gitea/modules/log" + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// ____ ____ .__ _____ +// \ \ / /___________|__|/ ____\__.__. +// \ Y // __ \_ __ \ \ __< | | +// \ /\ ___/| | \/ || | \___ | +// \___/ \___ >__| |__||__| / ____| +// \/ \/ + +// This file provides functions relating verifying gpg keys + +// VerifyGPGKey marks a GPG key as verified +func VerifyGPGKey(ownerID int64, keyID, token, signature string) (string, error) { + sess := x.NewSession() + defer sess.Close() + if err := sess.Begin(); err != nil { + return "", err + } + + key := new(GPGKey) + + has, err := sess.Where("owner_id = ? AND key_id = ?", ownerID, keyID).Get(key) + if err != nil { + return "", err + } else if !has { + return "", ErrGPGKeyNotExist{} + } + + sig, err := extractSignature(signature) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } + + signer, err := hashAndVerifyWithSubKeys(sig, token, key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n", key) + + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } + } + if signer == nil { + signer, err = hashAndVerifyWithSubKeys(sig, token+"\n\n", key) + if err != nil { + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + Wrapped: err, + } + } + } + + if signer == nil { + log.Error("Unable to validate token signature. Error: %v", err) + return "", ErrGPGInvalidTokenSignature{ + ID: key.KeyID, + } + } + + if signer.PrimaryKeyID != key.KeyID && signer.KeyID != key.KeyID { + return "", ErrGPGKeyNotExist{} + } + + key.Verified = true + if _, err := sess.ID(key.ID).SetExpr("verified", true).Update(new(GPGKey)); err != nil { + return "", err + } + + if err := sess.Commit(); err != nil { + return "", err + } + + return key.KeyID, nil +} From 4af7d8c6d9f8bbfa1df6f8233d14d06ee5e8acf1 Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 27 Jun 2021 17:12:43 +0100 Subject: [PATCH 22/26] Use common Verification Token code Signed-off-by: Andrew Thornton --- models/gpg_key_verify.go | 18 +++++++++++++++++- routers/api/v1/user/gpg_key.go | 13 +++++-------- routers/web/user/setting/keys.go | 12 +++++------- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/models/gpg_key_verify.go b/models/gpg_key_verify.go index 83743c74ed16e..8b4600a6bc4bb 100644 --- a/models/gpg_key_verify.go +++ b/models/gpg_key_verify.go @@ -4,7 +4,13 @@ package models -import "code.gitea.io/gitea/modules/log" +import ( + "strconv" + "time" + + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" +) // __________________ ________ ____ __. // / _____/\______ \/ _____/ | |/ _|____ ___.__. @@ -95,3 +101,13 @@ func VerifyGPGKey(ownerID int64, keyID, token, signature string) (string, error) return key.KeyID, nil } + +// VerificationToken returns token for the user that will be valid in minutes minutes time +func VerificationToken(user *User, minutes int) string { + return base.EncodeSha256( + time.Now().Truncate(1*time.Minute).Add(time.Duration(minutes)*time.Minute).Format(time.RFC1123Z) + ":" + + user.CreatedUnix.FormatLong() + ":" + + user.Name + ":" + + user.Email + ":" + + strconv.FormatInt(user.ID, 10)) +} diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go index 5a35ae7d4b08e..ec03e305ba1b2 100644 --- a/routers/api/v1/user/gpg_key.go +++ b/routers/api/v1/user/gpg_key.go @@ -7,11 +7,8 @@ package user import ( "fmt" "net/http" - "strconv" - "time" "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" @@ -123,8 +120,8 @@ func GetGPGKey(ctx *context.APIContext) { // CreateUserGPGKey creates new GPG key to given user by ID. func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) { - token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) - lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + token := models.VerificationToken(ctx.User, 1) + lastToken := models.VerificationToken(ctx.User, 0) keys, err := models.AddGPGKey(uid, form.ArmoredKey, token, form.Signature) if err != nil && models.IsErrGPGInvalidTokenSignature(err) { @@ -151,7 +148,7 @@ func GetVerificationToken(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + token := models.VerificationToken(ctx.User, 1) ctx.PlainText(http.StatusOK, []byte(token)) } @@ -173,8 +170,8 @@ func VerifyUserGPGKey(ctx *context.APIContext) { // "$ref": "#/responses/validationError" form := web.GetForm(ctx).(*api.VerifyGPGKeyOption) - token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) - lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + token := models.VerificationToken(ctx.User, 1) + lastToken := models.VerificationToken(ctx.User, 0) _, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature) if err != nil && models.IsErrGPGInvalidTokenSignature(err) { diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go index 4bf75946b7362..d875d84a7603f 100644 --- a/routers/web/user/setting/keys.go +++ b/routers/web/user/setting/keys.go @@ -7,8 +7,6 @@ package setting import ( "net/http" - "strconv" - "time" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" @@ -78,8 +76,8 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "gpg": - token := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) - lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + token := models.VerificationToken(ctx.User, 1) + lastToken := models.VerificationToken(ctx.User, 0) keys, err := models.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature) if err != nil && models.IsErrGPGInvalidTokenSignature(err) { @@ -125,8 +123,8 @@ func KeysPost(ctx *context.Context) { ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs)) ctx.Redirect(setting.AppSubURL + "/user/settings/keys") case "verify_gpg": - 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)) - lastToken := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + token := models.VerificationToken(ctx.User, 1) + lastToken := models.VerificationToken(ctx.User, 0) keyID, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature) if err != nil && models.IsErrGPGInvalidTokenSignature(err) { @@ -255,7 +253,7 @@ func loadKeysData(ctx *context.Context) { return } ctx.Data["GPGKeys"] = gpgkeys - tokenToSign := base.EncodeSha256(time.Now().Truncate(1*time.Minute).Add(1*time.Minute).Format(time.RFC1123Z) + ":" + ctx.User.CreatedUnix.FormatLong() + ":" + ctx.User.Name + ":" + ctx.User.Email + ":" + strconv.FormatInt(ctx.User.ID, 10)) + tokenToSign := models.VerificationToken(ctx.User, 1) // generate a new aes cipher using the csrfToken ctx.Data["TokenToSign"] = tokenToSign From 3bb22c404949607332b4836c2e2fa41fe087560d Mon Sep 17 00:00:00 2001 From: Andrew Thornton Date: Sun, 27 Jun 2021 18:57:54 +0100 Subject: [PATCH 23/26] fix tests Signed-off-by: Andrew Thornton --- integrations/api_gpg_keys_test.go | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/integrations/api_gpg_keys_test.go b/integrations/api_gpg_keys_test.go index 3296250bab170..8fc4124a4898e 100644 --- a/integrations/api_gpg_keys_test.go +++ b/integrations/api_gpg_keys_test.go @@ -29,10 +29,10 @@ func TestGPGKeys(t *testing.T) { results []int }{ {name: "NoLogin", makeRequest: MakeRequest, token: "", - results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized}, + results: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized}, }, {name: "LoggedAsUser2", makeRequest: session.MakeRequest, token: token, - results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusCreated}}, + results: []int{http.StatusOK, http.StatusOK, http.StatusNotFound, http.StatusNoContent, http.StatusUnprocessableEntity, http.StatusNotFound, http.StatusCreated, http.StatusNotFound, http.StatusCreated}}, } for _, tc := range tt { @@ -60,7 +60,7 @@ func TestGPGKeys(t *testing.T) { t.Run("CreateValidGPGKey", func(t *testing.T) { testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6]) }) - t.Run("CreateValidSecondaryEmailGPGKey", func(t *testing.T) { + t.Run("CreateValidSecondaryEmailGPGKeyNotActivated", func(t *testing.T) { testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[7]) }) }) @@ -74,12 +74,9 @@ func TestGPGKeys(t *testing.T) { req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) //GET all keys resp := session.MakeRequest(t, req, http.StatusOK) DecodeJSON(t, resp, &keys) - assert.Len(t, keys, 2) + assert.Len(t, keys, 1) - primaryKey1, primaryKey2 := keys[0], keys[1] //Primary key 1 - if primaryKey1.KeyID != "38EA3BCED732982C" { - primaryKey1, primaryKey2 = keys[1], keys[0] - } + primaryKey1 := keys[0] //Primary key 1 assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID) assert.Len(t, primaryKey1.Emails, 1) assert.EqualValues(t, "user2@example.com", primaryKey1.Emails[0].Email) @@ -89,11 +86,6 @@ func TestGPGKeys(t *testing.T) { assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID) assert.Empty(t, subKey.Emails) - assert.EqualValues(t, "3CEF46EF40BEFC3E", primaryKey2.KeyID) - assert.Len(t, primaryKey2.Emails, 1) - assert.EqualValues(t, "user2-2@example.com", primaryKey2.Emails[0].Email) - assert.False(t, primaryKey2.Emails[0].Verified) - var key api.GPGKey req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey1.ID, 10)+"?token="+token) //Primary key 1 resp = session.MakeRequest(t, req, http.StatusOK) @@ -108,15 +100,6 @@ func TestGPGKeys(t *testing.T) { DecodeJSON(t, resp, &key) assert.EqualValues(t, "70D7C694D17D03AD", key.KeyID) assert.Empty(t, key.Emails) - - req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey2.ID, 10)+"?token="+token) //Primary key 2 - resp = session.MakeRequest(t, req, http.StatusOK) - DecodeJSON(t, resp, &key) - assert.EqualValues(t, "3CEF46EF40BEFC3E", key.KeyID) - assert.Len(t, key.Emails, 1) - assert.EqualValues(t, "user2-2@example.com", key.Emails[0].Email) - assert.False(t, key.Emails[0].Verified) - }) //Check state after basic add From c728845acdd75d5605b4977730604e8b92ac30a4 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 27 Jun 2021 20:54:56 +0200 Subject: [PATCH 24/26] Update models/gpg_key_verify.go --- models/gpg_key_verify.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/gpg_key_verify.go b/models/gpg_key_verify.go index 8b4600a6bc4bb..15774dc058e86 100644 --- a/models/gpg_key_verify.go +++ b/models/gpg_key_verify.go @@ -102,7 +102,7 @@ func VerifyGPGKey(ownerID int64, keyID, token, signature string) (string, error) return key.KeyID, nil } -// VerificationToken returns token for the user that will be valid in minutes minutes time +// VerificationToken returns token for the user that will be valid in minutes (time) func VerificationToken(user *User, minutes int) string { return base.EncodeSha256( time.Now().Truncate(1*time.Minute).Add(time.Duration(minutes)*time.Minute).Format(time.RFC1123Z) + ":" + From ce2a2d2b7be438c9474776b3a2c91ff82866d0f8 Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Fri, 9 Jul 2021 14:35:39 -0400 Subject: [PATCH 25/26] Update models/gpg_key_add.go --- models/gpg_key_add.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/gpg_key_add.go b/models/gpg_key_add.go index 04ad7cb3002cc..1e589e7fee527 100644 --- a/models/gpg_key_add.go +++ b/models/gpg_key_add.go @@ -8,6 +8,7 @@ import ( "strings" "code.gitea.io/gitea/modules/log" + "github.com/keybase/go-crypto/openpgp" ) From 408ba967a9388ee6faf17f847857b7a1ee83756b Mon Sep 17 00:00:00 2001 From: techknowlogick Date: Fri, 9 Jul 2021 14:36:23 -0400 Subject: [PATCH 26/26] Update models/gpg_key_commit_verification.go --- models/gpg_key_commit_verification.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/gpg_key_commit_verification.go b/models/gpg_key_commit_verification.go index 17d22e58fa9cb..687c09b010dea 100644 --- a/models/gpg_key_commit_verification.go +++ b/models/gpg_key_commit_verification.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/keybase/go-crypto/openpgp/packet" )