mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-22 12:54:53 -05:00
Add option to provide signature for a token to verify key ownership (#14054)
* Add option to provide signed token to verify key ownership Currently we will only allow a key to be matched to a user if it matches an activated email address. This PR provides a different mechanism - if the user provides a signature for automatically generated token (based on the timestamp, user creation time, user ID, username and primary email. * Ensure verified keys can act for all active emails for the user * Add code to mark keys as verified * Slight UI adjustments * Slight UI adjustments 2 * Simplify signature verification slightly * fix postgres test * add api routes * handle swapped primary-keys * Verify the no-reply address for verified keys * Only add email addresses that are activated to keys * Fix committer shortcut properly * Restructure gpg_keys.go * Use common Verification Token code Signed-off-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
parent
67f135ca5d
commit
b82293270c
20 changed files with 1276 additions and 727 deletions
|
@ -29,10 +29,10 @@ func TestGPGKeys(t *testing.T) {
|
||||||
results []int
|
results []int
|
||||||
}{
|
}{
|
||||||
{name: "NoLogin", makeRequest: MakeRequest, token: "",
|
{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,
|
{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 {
|
for _, tc := range tt {
|
||||||
|
@ -60,7 +60,7 @@ func TestGPGKeys(t *testing.T) {
|
||||||
t.Run("CreateValidGPGKey", func(t *testing.T) {
|
t.Run("CreateValidGPGKey", func(t *testing.T) {
|
||||||
testCreateValidGPGKey(t, tc.makeRequest, tc.token, tc.results[6])
|
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])
|
testCreateValidSecondaryEmailGPGKey(t, tc.makeRequest, tc.token, tc.results[7])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -74,6 +74,7 @@ func TestGPGKeys(t *testing.T) {
|
||||||
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) //GET all keys
|
req := NewRequest(t, "GET", "/api/v1/user/gpg_keys?token="+token) //GET all keys
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
DecodeJSON(t, resp, &keys)
|
DecodeJSON(t, resp, &keys)
|
||||||
|
assert.Len(t, keys, 1)
|
||||||
|
|
||||||
primaryKey1 := keys[0] //Primary key 1
|
primaryKey1 := keys[0] //Primary key 1
|
||||||
assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID)
|
assert.EqualValues(t, "38EA3BCED732982C", primaryKey1.KeyID)
|
||||||
|
@ -85,12 +86,6 @@ func TestGPGKeys(t *testing.T) {
|
||||||
assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID)
|
assert.EqualValues(t, "70D7C694D17D03AD", subKey.KeyID)
|
||||||
assert.Empty(t, subKey.Emails)
|
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)
|
|
||||||
assert.False(t, primaryKey2.Emails[0].Verified)
|
|
||||||
|
|
||||||
var key api.GPGKey
|
var key api.GPGKey
|
||||||
req = NewRequest(t, "GET", "/api/v1/user/gpg_keys/"+strconv.FormatInt(primaryKey1.ID, 10)+"?token="+token) //Primary key 1
|
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)
|
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||||
|
@ -105,15 +100,6 @@ func TestGPGKeys(t *testing.T) {
|
||||||
DecodeJSON(t, resp, &key)
|
DecodeJSON(t, resp, &key)
|
||||||
assert.EqualValues(t, "70D7C694D17D03AD", key.KeyID)
|
assert.EqualValues(t, "70D7C694D17D03AD", key.KeyID)
|
||||||
assert.Empty(t, key.Emails)
|
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
|
//Check state after basic add
|
||||||
|
|
|
@ -451,6 +451,7 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
|
||||||
// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
|
// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
|
||||||
type ErrGPGNoEmailFound struct {
|
type ErrGPGNoEmailFound struct {
|
||||||
FailedEmails []string
|
FailedEmails []string
|
||||||
|
ID string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
|
// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
|
||||||
|
@ -463,6 +464,22 @@ func (err ErrGPGNoEmailFound) Error() string {
|
||||||
return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails)
|
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.
|
// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
|
||||||
type ErrGPGKeyParsing struct {
|
type ErrGPGKeyParsing struct {
|
||||||
ParseError error
|
ParseError error
|
||||||
|
|
|
@ -5,27 +5,25 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"container/list"
|
|
||||||
"crypto"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash"
|
|
||||||
"io"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
"github.com/keybase/go-crypto/openpgp"
|
"github.com/keybase/go-crypto/openpgp"
|
||||||
"github.com/keybase/go-crypto/openpgp/armor"
|
|
||||||
"github.com/keybase/go-crypto/openpgp/packet"
|
"github.com/keybase/go-crypto/openpgp/packet"
|
||||||
"xorm.io/xorm"
|
"xorm.io/xorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// __________________ ________ ____ __.
|
||||||
|
// / _____/\______ \/ _____/ | |/ _|____ ___.__.
|
||||||
|
// / \ ___ | ___/ \ ___ | <_/ __ < | |
|
||||||
|
// \ \_\ \| | \ \_\ \ | | \ ___/\___ |
|
||||||
|
// \______ /|____| \______ / |____|__ \___ > ____|
|
||||||
|
// \/ \/ \/ \/\/
|
||||||
|
|
||||||
// GPGKey represents a GPG key.
|
// GPGKey represents a GPG key.
|
||||||
type GPGKey struct {
|
type GPGKey struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
|
@ -38,18 +36,13 @@ type GPGKey struct {
|
||||||
AddedUnix timeutil.TimeStamp
|
AddedUnix timeutil.TimeStamp
|
||||||
SubsKey []*GPGKey `xorm:"-"`
|
SubsKey []*GPGKey `xorm:"-"`
|
||||||
Emails []*EmailAddress
|
Emails []*EmailAddress
|
||||||
|
Verified bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
CanSign bool
|
CanSign bool
|
||||||
CanEncryptComms bool
|
CanEncryptComms bool
|
||||||
CanEncryptStorage bool
|
CanEncryptStorage bool
|
||||||
CanCertify bool
|
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
|
// BeforeInsert will be invoked by XORM before inserting a record
|
||||||
func (key *GPGKey) BeforeInsert() {
|
func (key *GPGKey) BeforeInsert() {
|
||||||
key.AddedUnix = timeutil.TimeStampNow()
|
key.AddedUnix = timeutil.TimeStampNow()
|
||||||
|
@ -96,131 +89,6 @@ func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) {
|
||||||
return keys, x.Where("key_id=?", keyID).Find(&keys)
|
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 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))
|
|
||||||
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)
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// GPGKeyToEntity retrieve the imported key and the traducted entity
|
||||||
func GPGKeyToEntity(k *GPGKey) (*openpgp.Entity, error) {
|
func GPGKeyToEntity(k *GPGKey) (*openpgp.Entity, error) {
|
||||||
impKey, err := GetGPGImportByKeyID(k.KeyID)
|
impKey, err := GetGPGImportByKeyID(k.KeyID)
|
||||||
|
@ -254,27 +122,8 @@ func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, e
|
||||||
}, nil
|
}, 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)
|
// 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, verified bool) (*GPGKey, error) {
|
||||||
pubkey := e.PrimaryKey
|
pubkey := e.PrimaryKey
|
||||||
expiry := getExpiryTime(e)
|
expiry := getExpiryTime(e)
|
||||||
|
|
||||||
|
@ -301,20 +150,22 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
|
||||||
}
|
}
|
||||||
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
|
email := strings.ToLower(strings.TrimSpace(ident.UserId.Email))
|
||||||
for _, e := range userEmails {
|
for _, e := range userEmails {
|
||||||
if e.LowerEmail == email {
|
if e.IsActivated && e.LowerEmail == email {
|
||||||
emails = append(emails, e)
|
emails = append(emails, e)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
// In the case no email as been found
|
// In the case no email as been found
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
failedEmails := make([]string, 0, len(e.Identities))
|
failedEmails := make([]string, 0, len(e.Identities))
|
||||||
for _, ident := range e.Identities {
|
for _, ident := range e.Identities {
|
||||||
failedEmails = append(failedEmails, ident.UserId.Email)
|
failedEmails = append(failedEmails, ident.UserId.Email)
|
||||||
}
|
}
|
||||||
return nil, ErrGPGNoEmailFound{failedEmails}
|
return nil, ErrGPGNoEmailFound{failedEmails, e.PrimaryKey.KeyIdString()}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := base64EncPubKey(pubkey)
|
content, err := base64EncPubKey(pubkey)
|
||||||
|
@ -330,6 +181,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) {
|
||||||
ExpiredUnix: timeutil.TimeStamp(expiry.Unix()),
|
ExpiredUnix: timeutil.TimeStamp(expiry.Unix()),
|
||||||
Emails: emails,
|
Emails: emails,
|
||||||
SubsKey: subkeys,
|
SubsKey: subkeys,
|
||||||
|
Verified: verified,
|
||||||
CanSign: pubkey.CanSign(),
|
CanSign: pubkey.CanSign(),
|
||||||
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
|
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||||
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
|
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
|
||||||
|
@ -378,545 +230,32 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
|
||||||
return sess.Commit()
|
return sess.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitVerification represents a commit validation of signature
|
func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) {
|
||||||
type CommitVerification struct {
|
uid := int64(0)
|
||||||
Verified bool
|
var userEmails []*EmailAddress
|
||||||
Warning bool
|
var user *User
|
||||||
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, committer, signer *User, email string) *CommitVerification {
|
|
||||||
// Generating hash of commit
|
|
||||||
hash, err := populateHash(sig.Hash, []byte(payload))
|
|
||||||
if err != nil { // Skipping failed to generate hash
|
|
||||||
log.Error("PopulateHash: %v", err)
|
|
||||||
return &CommitVerification{
|
|
||||||
CommittingUser: committer,
|
|
||||||
Verified: false,
|
|
||||||
Reason: "gpg.error.generate_hash",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := verifySign(sig, hash, k); err == nil {
|
|
||||||
return &CommitVerification{ // Everything is ok
|
|
||||||
CommittingUser: committer,
|
|
||||||
Verified: true,
|
|
||||||
Reason: fmt.Sprintf("%s / %s", signer.Name, k.KeyID),
|
|
||||||
SigningUser: signer,
|
|
||||||
SigningKey: k,
|
|
||||||
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 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 {
|
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 := false
|
|
||||||
if len(email) != 0 {
|
|
||||||
for _, e := range key.Emails {
|
for _, e := range key.Emails {
|
||||||
if e.IsActivated && strings.EqualFold(e.Email, email) {
|
if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) {
|
||||||
activated = true
|
return true, e.Email
|
||||||
email = e.Email
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !activated {
|
if key.Verified && key.OwnerID != 0 {
|
||||||
for _, pkey := range primaryKeys {
|
if uid != key.OwnerID {
|
||||||
for _, e := range pkey.Emails {
|
userEmails, _ = GetEmailAddresses(key.OwnerID)
|
||||||
if e.IsActivated && strings.EqualFold(e.Email, email) {
|
uid = key.OwnerID
|
||||||
activated = true
|
user = &User{ID: uid}
|
||||||
email = e.Email
|
_, _ = GetUser(user)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
for _, e := range userEmails {
|
||||||
|
if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) {
|
||||||
|
return true, e.Email
|
||||||
}
|
}
|
||||||
if activated {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) {
|
||||||
|
return true, user.GetEmail()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for _, e := range key.Emails {
|
|
||||||
if e.IsActivated {
|
|
||||||
activated = true
|
|
||||||
email = e.Email
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
return false, email
|
||||||
if !activated {
|
|
||||||
for _, pkey := range primaryKeys {
|
|
||||||
for _, e := range pkey.Emails {
|
|
||||||
if e.IsActivated {
|
|
||||||
activated = true
|
|
||||||
email = e.Email
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if activated {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := hashAndVerifyWithSubKeys(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 committer
|
|
||||||
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",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
// Pre-check (& optimization) that emails attached to key can be attached to the committer 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 !canValidate {
|
|
||||||
continue // Skip this key
|
|
||||||
}
|
|
||||||
|
|
||||||
commitVerification := hashAndVerifyWithSubKeys(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 := hashAndVerifyWithSubKeys(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
|
|
||||||
}
|
}
|
||||||
|
|
125
models/gpg_key_add.go
Normal file
125
models/gpg_key_add.go
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// 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()
|
||||||
|
}
|
520
models/gpg_key_commit_verification.go
Normal file
520
models/gpg_key_commit_verification.go
Normal file
|
@ -0,0 +1,520 @@
|
||||||
|
// 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 committer
|
||||||
|
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 committer 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
|
||||||
|
}
|
137
models/gpg_key_common.go
Normal file
137
models/gpg_key_common.go
Normal file
|
@ -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
|
||||||
|
}
|
38
models/gpg_key_import.go
Normal file
38
models/gpg_key_import.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -227,7 +227,7 @@ Q0KHb+QcycSgbDx0ZAvdIacuKvBBcbxrsmFUI4LR+oIup0G9gUc0roPvr014jYQL
|
||||||
=zHo9
|
=zHo9
|
||||||
-----END PGP PUBLIC KEY BLOCK-----`
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
|
|
||||||
keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters)
|
keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters, "", "")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
if assert.NotEmpty(t, keys) {
|
if assert.NotEmpty(t, keys) {
|
||||||
key := keys[0]
|
key := keys[0]
|
||||||
|
|
113
models/gpg_key_verify.go
Normal file
113
models/gpg_key_verify.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// 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 (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) + ":" +
|
||||||
|
user.CreatedUnix.FormatLong() + ":" +
|
||||||
|
user.Name + ":" +
|
||||||
|
user.Email + ":" +
|
||||||
|
strconv.FormatInt(user.ID, 10))
|
||||||
|
}
|
|
@ -325,6 +325,8 @@ var migrations = []Migration{
|
||||||
NewMigration("Create protected tag table", createProtectedTagTable),
|
NewMigration("Create protected tag table", createProtectedTagTable),
|
||||||
// v187 -> v188
|
// v187 -> v188
|
||||||
NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
|
NewMigration("Drop unneeded webhook related columns", dropWebhookColumns),
|
||||||
|
// v188 -> v189
|
||||||
|
NewMigration("Add key is verified to gpg key", addKeyIsVerified),
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDBVersion returns the current db version
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|
15
models/migrations/v188.go
Normal file
15
models/migrations/v188.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
// 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 migrations
|
||||||
|
|
||||||
|
import "xorm.io/xorm"
|
||||||
|
|
||||||
|
func addKeyIsVerified(x *xorm.Engine) error {
|
||||||
|
type GPGKey struct {
|
||||||
|
Verified bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return x.Sync(new(GPGKey))
|
||||||
|
}
|
|
@ -191,6 +191,7 @@ func ToGPGKey(key *models.GPGKey) *api.GPGKey {
|
||||||
CanEncryptComms: k.CanEncryptComms,
|
CanEncryptComms: k.CanEncryptComms,
|
||||||
CanEncryptStorage: k.CanEncryptStorage,
|
CanEncryptStorage: k.CanEncryptStorage,
|
||||||
CanCertify: k.CanSign,
|
CanCertify: k.CanSign,
|
||||||
|
Verified: k.Verified,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emails := make([]*api.GPGKeyEmail, len(key.Emails))
|
emails := make([]*api.GPGKeyEmail, len(key.Emails))
|
||||||
|
@ -210,6 +211,7 @@ func ToGPGKey(key *models.GPGKey) *api.GPGKey {
|
||||||
CanEncryptComms: key.CanEncryptComms,
|
CanEncryptComms: key.CanEncryptComms,
|
||||||
CanEncryptStorage: key.CanEncryptStorage,
|
CanEncryptStorage: key.CanEncryptStorage,
|
||||||
CanCertify: key.CanSign,
|
CanCertify: key.CanSign,
|
||||||
|
Verified: key.Verified,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ type GPGKey struct {
|
||||||
CanEncryptComms bool `json:"can_encrypt_comms"`
|
CanEncryptComms bool `json:"can_encrypt_comms"`
|
||||||
CanEncryptStorage bool `json:"can_encrypt_storage"`
|
CanEncryptStorage bool `json:"can_encrypt_storage"`
|
||||||
CanCertify bool `json:"can_certify"`
|
CanCertify bool `json:"can_certify"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
Created time.Time `json:"created_at,omitempty"`
|
Created time.Time `json:"created_at,omitempty"`
|
||||||
// swagger:strfmt date-time
|
// swagger:strfmt date-time
|
||||||
|
@ -40,4 +41,14 @@ type CreateGPGKeyOption struct {
|
||||||
// required: true
|
// required: true
|
||||||
// unique: true
|
// unique: true
|
||||||
ArmoredKey string `json:"armored_public_key" binding:"Required"`
|
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"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -595,7 +595,20 @@ 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_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.
|
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_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 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 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
|
||||||
|
gpg_token = Token
|
||||||
|
gpg_token_help = You can generate a signature using:
|
||||||
|
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-----'
|
||||||
|
verify_gpg_key_success = The GPG key '%s' has been verified.
|
||||||
subkeys = Subkeys
|
subkeys = Subkeys
|
||||||
key_id = Key ID
|
key_id = Key ID
|
||||||
key_name = Key Name
|
key_name = Key Name
|
||||||
|
|
|
@ -686,6 +686,9 @@ func Routes() *web.Route {
|
||||||
Delete(user.DeleteGPGKey)
|
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).
|
m.Combo("/repos").Get(user.ListMyRepos).
|
||||||
Post(bind(api.CreateRepoOption{}), repo.Create)
|
Post(bind(api.CreateRepoOption{}), repo.Create)
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
@ -119,14 +120,84 @@ func GetGPGKey(ctx *context.APIContext) {
|
||||||
|
|
||||||
// CreateUserGPGKey creates new GPG key to given user by ID.
|
// CreateUserGPGKey creates new GPG key to given user by ID.
|
||||||
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
|
func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
|
||||||
keys, err := models.AddGPGKey(uid, form.ArmoredKey)
|
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) {
|
||||||
|
keys, err = models.AddGPGKey(uid, form.ArmoredKey, lastToken, form.Signature)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
HandleAddGPGKeyError(ctx, err)
|
HandleAddGPGKeyError(ctx, err, token)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(http.StatusCreated, convert.ToGPGKey(keys[0]))
|
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 := models.VerificationToken(ctx.User, 1)
|
||||||
|
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 := 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) {
|
||||||
|
_, 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
|
// swagger:parameters userCurrentPostGPGKey
|
||||||
type swaggerUserCurrentPostGPGKey struct {
|
type swaggerUserCurrentPostGPGKey struct {
|
||||||
// in:body
|
// in:body
|
||||||
|
@ -189,7 +260,7 @@ func DeleteGPGKey(ctx *context.APIContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleAddGPGKeyError handle add GPGKey error
|
// HandleAddGPGKeyError handle add GPGKey error
|
||||||
func HandleAddGPGKeyError(ctx *context.APIContext, err error) {
|
func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) {
|
||||||
switch {
|
switch {
|
||||||
case models.IsErrGPGKeyAccessDenied(err):
|
case models.IsErrGPGKeyAccessDenied(err):
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key")
|
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key")
|
||||||
|
@ -198,7 +269,9 @@ func HandleAddGPGKeyError(ctx *context.APIContext, err error) {
|
||||||
case models.IsErrGPGKeyParsing(err):
|
case models.IsErrGPGKeyParsing(err):
|
||||||
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err)
|
ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err)
|
||||||
case models.IsErrGPGNoEmailFound(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:
|
default:
|
||||||
ctx.Error(http.StatusInternalServerError, "AddGPGKey", err)
|
ctx.Error(http.StatusInternalServerError, "AddGPGKey", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,13 @@ func KeysPost(ctx *context.Context) {
|
||||||
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
|
ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||||
case "gpg":
|
case "gpg":
|
||||||
keys, err := models.AddGPGKey(ctx.User.ID, form.Content)
|
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) {
|
||||||
|
keys, err = models.AddGPGKey(ctx.User.ID, form.Content, lastToken, form.Signature)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Data["HasGPGError"] = true
|
ctx.Data["HasGPGError"] = true
|
||||||
switch {
|
switch {
|
||||||
|
@ -88,10 +94,18 @@ func KeysPost(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["Err_Content"] = true
|
ctx.Data["Err_Content"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
|
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):
|
case models.IsErrGPGNoEmailFound(err):
|
||||||
loadKeysData(ctx)
|
loadKeysData(ctx)
|
||||||
|
|
||||||
ctx.Data["Err_Content"] = true
|
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)
|
ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
|
||||||
default:
|
default:
|
||||||
ctx.ServerError("AddPublicKey", err)
|
ctx.ServerError("AddPublicKey", err)
|
||||||
|
@ -108,6 +122,29 @@ func KeysPost(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
|
ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
|
||||||
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||||
|
case "verify_gpg":
|
||||||
|
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) {
|
||||||
|
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.KeyID
|
||||||
|
ctx.Data["Err_Signature"] = true
|
||||||
|
ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID
|
||||||
|
ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
|
||||||
|
default:
|
||||||
|
ctx.ServerError("VerifyGPG", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
|
||||||
|
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
|
||||||
case "ssh":
|
case "ssh":
|
||||||
content, err := models.CheckPublicKeyString(form.Content)
|
content, err := models.CheckPublicKeyString(form.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -216,6 +253,10 @@ func loadKeysData(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["GPGKeys"] = gpgkeys
|
ctx.Data["GPGKeys"] = gpgkeys
|
||||||
|
tokenToSign := models.VerificationToken(ctx.User, 1)
|
||||||
|
|
||||||
|
// generate a new aes cipher using the csrfToken
|
||||||
|
ctx.Data["TokenToSign"] = tokenToSign
|
||||||
|
|
||||||
principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
|
principals, err := models.ListPrincipalKeys(ctx.User.ID, models.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -223,4 +264,6 @@ func loadKeysData(ctx *context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["Principals"] = principals
|
ctx.Data["Principals"] = principals
|
||||||
|
|
||||||
|
ctx.Data["VerifyingID"] = ctx.Query("verify_gpg")
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,6 +326,8 @@ type AddKeyForm struct {
|
||||||
Type string `binding:"OmitEmpty"`
|
Type string `binding:"OmitEmpty"`
|
||||||
Title string `binding:"Required;MaxSize(50)"`
|
Title string `binding:"Required;MaxSize(50)"`
|
||||||
Content string `binding:"Required"`
|
Content string `binding:"Required"`
|
||||||
|
Signature string `binding:"OmitEmpty"`
|
||||||
|
KeyID string `binding:"OmitEmpty"`
|
||||||
IsWritable bool
|
IsWritable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10714,6 +10714,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": {
|
"/user/gpg_keys": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -12826,6 +12872,10 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"x-go-name": "ArmoredKey"
|
"x-go-name": "ArmoredKey"
|
||||||
|
},
|
||||||
|
"armored_signature": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Signature"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
@ -14484,6 +14534,10 @@
|
||||||
"$ref": "#/definitions/GPGKey"
|
"$ref": "#/definitions/GPGKey"
|
||||||
},
|
},
|
||||||
"x-go-name": "SubsKey"
|
"x-go-name": "SubsKey"
|
||||||
|
},
|
||||||
|
"verified": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-go-name": "Verified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
|
|
@ -15,12 +15,20 @@
|
||||||
<button class="ui red tiny button delete-button" id="delete-gpg" data-url="{{$.Link}}/delete?type=gpg" data-id="{{.ID}}">
|
<button class="ui red tiny button delete-button" id="delete-gpg" data-url="{{$.Link}}/delete?type=gpg" data-id="{{.ID}}">
|
||||||
{{$.i18n.Tr "settings.delete_key"}}
|
{{$.i18n.Tr "settings.delete_key"}}
|
||||||
</button>
|
</button>
|
||||||
|
{{if and (not .Verified) (ne $.VerifyingID .KeyID)}}
|
||||||
|
<a class="ui blue tiny show-panel button" href="{{$.Link}}?verify_gpg={{.KeyID}}">{{$.i18n.Tr "settings.gpg_key_verify"}}</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
<div class="left floated content">
|
<div class="left floated content">
|
||||||
<span class="{{if or .ExpiredUnix.IsZero ($.PageStartTime.Before .ExpiredUnix.AsTime)}}green{{end}}">{{svg "octicon-key" 32}}</span>
|
<span class="{{if or .ExpiredUnix.IsZero ($.PageStartTime.Before .ExpiredUnix.AsTime)}}green{{end}}">{{svg "octicon-key" 32}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{range .Emails}}<strong>{{.Email}} </strong>{{end}}
|
{{if .Verified}}
|
||||||
|
<span class="poping up" data-content="{{$.i18n.Tr "settings.gpg_key_verified_long"}}">{{svg "octicon-shield-check"}} <strong>{{$.i18n.Tr "settings.gpg_key_verified"}}</strong></span>
|
||||||
|
{{end}}
|
||||||
|
{{if gt (len .Emails) 0}}
|
||||||
|
<span class="poping up" data-content="{{$.i18n.Tr "settings.gpg_key_matched_identities_long"}}">{{svg "octicon-mail"}} {{$.i18n.Tr "settings.gpg_key_matched_identities"}} {{range .Emails}}<strong>{{.Email}} </strong>{{end}}</span>
|
||||||
|
{{end}}
|
||||||
<div class="print meta">
|
<div class="print meta">
|
||||||
<b>{{$.i18n.Tr "settings.key_id"}}:</b> {{.KeyID}}
|
<b>{{$.i18n.Tr "settings.key_id"}}:</b> {{.KeyID}}
|
||||||
<b>{{$.i18n.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.KeyID}} {{end}}
|
<b>{{$.i18n.Tr "settings.subkeys"}}:</b> {{range .SubsKey}} {{.KeyID}} {{end}}
|
||||||
|
@ -32,6 +40,37 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{{if and (not .Verified) (eq $.VerifyingID .KeyID)}}
|
||||||
|
<div class="ui segment">
|
||||||
|
<h4>{{$.i18n.Tr "settings.gpg_token_required"}}</h4>
|
||||||
|
<form class="ui form{{if $.HasGPGVerifyError}} error{{end}}" action="{{$.Link}}" method="post">
|
||||||
|
{{$.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="title" value="none">
|
||||||
|
<input type="hidden" name="content" value="{{.KeyID}}">
|
||||||
|
<input type="hidden" name="key_id" value="{{.KeyID}}">
|
||||||
|
<div class="field">
|
||||||
|
<label for="token">{{$.i18n.Tr "settings.gpg_token"}}</label>
|
||||||
|
<input readonly="" value="{{$.TokenToSign}}">
|
||||||
|
<div class="help">
|
||||||
|
<p>{{$.i18n.Tr "settings.gpg_token_help"}}</p>
|
||||||
|
<p><code>{{$.i18n.Tr "settings.gpg_token_code" $.TokenToSign .KeyID}}</code></p>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="signature">{{$.i18n.Tr "settings.gpg_token_signature"}}</label>
|
||||||
|
<textarea id="gpg-key-signature" name="signature" placeholder="{{$.i18n.Tr "settings.key_signature_gpg_placeholder"}}" required>{{$.signature}}</textarea>
|
||||||
|
</div>
|
||||||
|
<input name="type" type="hidden" value="verify_gpg">
|
||||||
|
<button class="ui green button">
|
||||||
|
{{$.i18n.Tr "settings.gpg_key_verify"}}
|
||||||
|
</button>
|
||||||
|
<a class="ui red button" href="{{$.Link}}">
|
||||||
|
{{$.i18n.Tr "settings.cancel"}}
|
||||||
|
</a>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,13 +81,30 @@
|
||||||
{{.i18n.Tr "settings.add_new_gpg_key"}}
|
{{.i18n.Tr "settings.add_new_gpg_key"}}
|
||||||
</h4>
|
</h4>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
<form class="ui form" action="{{.Link}}" method="post">
|
<form class="ui form{{if .HasGPGError}} error{{end}}" action="{{.Link}}" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
<input type="hidden" name="title" value="none">
|
<input type="hidden" name="title" value="none">
|
||||||
<div class="field {{if .Err_Content}}error{{end}}">
|
<div class="field {{if .Err_Content}}error{{end}}">
|
||||||
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
|
<label for="content">{{.i18n.Tr "settings.key_content"}}</label>
|
||||||
<textarea id="gpg-key-content" name="content" placeholder="{{.i18n.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
|
<textarea id="gpg-key-content" name="content" placeholder="{{.i18n.Tr "settings.key_content_gpg_placeholder"}}" required>{{.content}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
{{if .Err_Signature}}
|
||||||
|
<div class="ui error message">
|
||||||
|
<p>{{.i18n.Tr "settings.gpg_token_required"}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="token">{{.i18n.Tr "setting.gpg_token"}}
|
||||||
|
<input readonly="" value="{{.TokenToSign}}">
|
||||||
|
<div class="help">
|
||||||
|
<p>{{.i18n.Tr "settings.gpg_token_help"}}</p>
|
||||||
|
<p><code>{{$.i18n.Tr "settings.gpg_token_code" .TokenToSign .KeyID}}</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="signature">{{.i18n.Tr "settings.gpg_token_signature"}}</label>
|
||||||
|
<textarea id="gpg-key-signature" name="signature" placeholder="{{.i18n.Tr "settings.key_signature_gpg_placeholder"}}" required>{{.signature}}</textarea>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
<input name="type" type="hidden" value="gpg">
|
<input name="type" type="hidden" value="gpg">
|
||||||
<button class="ui green button">
|
<button class="ui green button">
|
||||||
{{.i18n.Tr "settings.add_key"}}
|
{{.i18n.Tr "settings.add_key"}}
|
||||||
|
|
Loading…
Reference in a new issue