diff options
author | zeripath <art27@cantab.net> | 2021-07-13 14:28:07 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-13 15:28:07 +0200 |
commit | b82293270c7d2d36d79cb9c5731d07c3f5b33f6b (patch) | |
tree | a79131e08ecf19cc8e642fcc032bfee0e30959c0 /models | |
parent | 67f135ca5ddfcab4391a00af4936d0260079cd97 (diff) | |
download | gitea-b82293270c7d2d36d79cb9c5731d07c3f5b33f6b.tar.gz gitea-b82293270c7d2d36d79cb9c5731d07c3f5b33f6b.zip |
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>
Diffstat (limited to 'models')
-rw-r--r-- | models/error.go | 17 | ||||
-rw-r--r-- | models/gpg_key.go | 737 | ||||
-rw-r--r-- | models/gpg_key_add.go | 125 | ||||
-rw-r--r-- | models/gpg_key_commit_verification.go | 520 | ||||
-rw-r--r-- | models/gpg_key_common.go | 137 | ||||
-rw-r--r-- | models/gpg_key_import.go | 38 | ||||
-rw-r--r-- | models/gpg_key_test.go | 2 | ||||
-rw-r--r-- | models/gpg_key_verify.go | 113 | ||||
-rw-r--r-- | models/migrations/migrations.go | 2 | ||||
-rw-r--r-- | models/migrations/v188.go | 15 |
10 files changed, 1006 insertions, 700 deletions
diff --git a/models/error.go b/models/error.go index 7b99dc6592..fd8f2771ae 100644 --- a/models/error.go +++ b/models/error.go @@ -451,6 +451,7 @@ func (err ErrKeyNameAlreadyUsed) Error() string { // ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error. type ErrGPGNoEmailFound struct { FailedEmails []string + ID string } // IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound. @@ -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) } +// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error. +type ErrGPGInvalidTokenSignature struct { + Wrapped error + ID string +} + +// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature. +func IsErrGPGInvalidTokenSignature(err error) bool { + _, ok := err.(ErrGPGInvalidTokenSignature) + return ok +} + +func (err ErrGPGInvalidTokenSignature) Error() string { + return "the provided signature does not sign the token with the provided key" +} + // ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error. type ErrGPGKeyParsing struct { ParseError error diff --git a/models/gpg_key.go b/models/gpg_key.go index 140a71df6d..74ffb82a54 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -5,27 +5,25 @@ package models import ( - "bytes" - "container/list" - "crypto" - "encoding/base64" "fmt" - "hash" - "io" "strings" "time" - "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" "github.com/keybase/go-crypto/openpgp" - "github.com/keybase/go-crypto/openpgp/armor" "github.com/keybase/go-crypto/openpgp/packet" "xorm.io/xorm" ) +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ + // GPGKey represents a GPG key. type GPGKey struct { ID int64 `xorm:"pk autoincr"` @@ -38,18 +36,13 @@ type GPGKey struct { AddedUnix timeutil.TimeStamp SubsKey []*GPGKey `xorm:"-"` Emails []*EmailAddress + Verified bool `xorm:"NOT NULL DEFAULT false"` CanSign bool CanEncryptComms bool CanEncryptStorage 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 func (key *GPGKey) BeforeInsert() { key.AddedUnix = timeutil.TimeStampNow() @@ -96,131 +89,6 @@ func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) { return keys, x.Where("key_id=?", keyID).Find(&keys) } -// GetGPGImportByKeyID returns the import public armored key by given KeyID. -func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) { - key := new(GPGKeyImport) - has, err := x.ID(keyID).Get(key) - if err != nil { - return nil, err - } else if !has { - return nil, ErrGPGKeyImportNotExist{keyID} - } - return key, nil -} - -// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. -// The function returns the actual public key on success -func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) { - list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) - if err != nil { - return nil, ErrGPGKeyParsing{err} - } - return list, nil -} - -// addGPGKey add key, import and subkeys to database -func addGPGKey(e Engine, key *GPGKey, content string) (err error) { - // Add GPGKeyImport - if _, err = e.Insert(GPGKeyImport{ - KeyID: key.KeyID, - Content: content, - }); err != nil { - return err - } - // Save GPG primary key. - if _, err = e.Insert(key); err != nil { - return err - } - // Save GPG subs key. - for _, subkey := range key.SubsKey { - if err := addGPGSubKey(e, subkey); err != nil { - return err - } - } - return nil -} - -// addGPGSubKey add subkeys to database -func addGPGSubKey(e Engine, key *GPGKey) (err error) { - // Save GPG primary key. - if _, err = e.Insert(key); err != nil { - return err - } - // Save GPG subs key. - for _, subkey := range key.SubsKey { - if err := addGPGSubKey(e, subkey); err != nil { - return err - } - } - return nil -} - -// AddGPGKey adds new public key to database. -func AddGPGKey(ownerID int64, content 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 func GPGKeyToEntity(k *GPGKey) (*openpgp.Entity, error) { impKey, err := GetGPGImportByKeyID(k.KeyID) @@ -254,27 +122,8 @@ func parseSubGPGKey(ownerID int64, primaryID string, pubkey *packet.PublicKey, e }, nil } -// getExpiryTime extract the expire time of primary key based on sig -func getExpiryTime(e *openpgp.Entity) time.Time { - expiry := time.Time{} - // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 - var selfSig *packet.Signature - for _, ident := range e.Identities { - if selfSig == nil { - selfSig = ident.SelfSignature - } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { - selfSig = ident.SelfSignature - break - } - } - if selfSig.KeyLifetimeSecs != nil { - expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) - } - return expiry -} - // parseGPGKey parse a PrimaryKey entity (primary key + subs keys + self-signature) -func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { +func parseGPGKey(ownerID int64, e *openpgp.Entity, verified bool) (*GPGKey, error) { pubkey := e.PrimaryKey expiry := getExpiryTime(e) @@ -301,20 +150,22 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { } email := strings.ToLower(strings.TrimSpace(ident.UserId.Email)) for _, e := range userEmails { - if e.LowerEmail == email { + if e.IsActivated && e.LowerEmail == email { emails = append(emails, e) break } } } - // In the case no email as been found - if len(emails) == 0 { - failedEmails := make([]string, 0, len(e.Identities)) - for _, ident := range e.Identities { - failedEmails = append(failedEmails, ident.UserId.Email) + if !verified { + // In the case no email as been found + if len(emails) == 0 { + failedEmails := make([]string, 0, len(e.Identities)) + for _, ident := range e.Identities { + failedEmails = append(failedEmails, ident.UserId.Email) + } + return nil, ErrGPGNoEmailFound{failedEmails, e.PrimaryKey.KeyIdString()} } - return nil, ErrGPGNoEmailFound{failedEmails} } content, err := base64EncPubKey(pubkey) @@ -330,6 +181,7 @@ func parseGPGKey(ownerID int64, e *openpgp.Entity) (*GPGKey, error) { ExpiredUnix: timeutil.TimeStamp(expiry.Unix()), Emails: emails, SubsKey: subkeys, + Verified: verified, CanSign: pubkey.CanSign(), CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(), CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(), @@ -378,545 +230,32 @@ func DeleteGPGKey(doer *User, id int64) (err error) { return sess.Commit() } -// CommitVerification represents a commit validation of signature -type CommitVerification struct { - Verified bool - Warning bool - Reason string - SigningUser *User - CommittingUser *User - SigningEmail string - SigningKey *GPGKey - TrustStatus string -} - -// SignCommit represents a commit with validation of signature. -type SignCommit struct { - Verification *CommitVerification - *UserCommit -} - -const ( - // BadSignature is used as the reason when the signature has a KeyID that is in the db - // but no key that has that ID verifies the signature. This is a suspicious failure. - BadSignature = "gpg.error.probable_bad_signature" - // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the - // default Key but is not verified by the default key. This is a suspicious failure. - BadDefaultSignature = "gpg.error.probable_bad_default_signature" - // NoKeyFound is used as the reason when no key can be found to verify the signature. - NoKeyFound = "gpg.error.no_gpg_keys_found" -) - -func readerFromBase64(s string) (io.Reader, error) { - bs, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return nil, err - } - return bytes.NewBuffer(bs), nil -} - -func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) { - h := hashFunc.New() - if _, err := h.Write(msg); err != nil { - return nil, err - } - return h, nil -} - -// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17 -func readArmoredSign(r io.Reader) (body io.Reader, err error) { - block, err := armor.Decode(r) - if err != nil { - return - } - if block.Type != openpgp.SignatureType { - return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type) - } - return block.Body, nil -} - -func extractSignature(s string) (*packet.Signature, error) { - r, err := readArmoredSign(strings.NewReader(s)) - if err != nil { - return nil, fmt.Errorf("Failed to read signature armor") - } - p, err := packet.Read(r) - if err != nil { - return nil, fmt.Errorf("Failed to read signature packet") - } - sig, ok := p.(*packet.Signature) - if !ok { - return nil, fmt.Errorf("Packet is not a signature") - } - return sig, nil -} - -func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error { - // Check if key can sign - if !k.CanSign { - return fmt.Errorf("key can not sign") - } - // Decode key - pkey, err := base64DecPubKey(k.Content) - if err != nil { - return err - } - return pkey.VerifySignature(h, s) -} - -func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, 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 - } +func checkKeyEmails(email string, keys ...*GPGKey) (bool, string) { + uid := int64(0) + var userEmails []*EmailAddress + var user *User 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 { - if e.IsActivated && strings.EqualFold(e.Email, email) { - activated = true - email = e.Email - break - } - } - if !activated { - for _, pkey := range primaryKeys { - for _, e := range pkey.Emails { - if e.IsActivated && strings.EqualFold(e.Email, email) { - activated = true - email = e.Email - break - } - } - if activated { - break - } - } - } - } else { - for _, e := range key.Emails { - if e.IsActivated { - activated = true - email = e.Email - break - } - } - 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", - } + for _, e := range key.Emails { + if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { + return true, e.Email } - - } - } - - // 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", + if key.Verified && key.OwnerID != 0 { + if uid != key.OwnerID { + userEmails, _ = GetEmailAddresses(key.OwnerID) + uid = key.OwnerID + user = &User{ID: uid} + _, _ = GetUser(user) } - } - - 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 + for _, e := range userEmails { + if e.IsActivated && (email == "" || strings.EqualFold(e.Email, email)) { + return true, e.Email } } - if !canValidate { - continue // Skip this key - } - - commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email) - if commitVerification != nil { - return commitVerification + if user.KeepEmailPrivate && strings.EqualFold(email, user.GetEmail()) { + return true, user.GetEmail() } } } - - 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 + return false, email } diff --git a/models/gpg_key_add.go b/models/gpg_key_add.go new file mode 100644 index 0000000000..1e589e7fee --- /dev/null +++ b/models/gpg_key_add.go @@ -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() +} diff --git a/models/gpg_key_commit_verification.go b/models/gpg_key_commit_verification.go new file mode 100644 index 0000000000..f0c27f13aa --- /dev/null +++ b/models/gpg_key_commit_verification.go @@ -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 +} diff --git a/models/gpg_key_common.go b/models/gpg_key_common.go new file mode 100644 index 0000000000..72803625ee --- /dev/null +++ b/models/gpg_key_common.go @@ -0,0 +1,137 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +import ( + "bytes" + "crypto" + "encoding/base64" + "fmt" + "hash" + "io" + "strings" + "time" + + "github.com/keybase/go-crypto/openpgp" + "github.com/keybase/go-crypto/openpgp/armor" + "github.com/keybase/go-crypto/openpgp/packet" +) + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// _________ +// \_ ___ \ ____ _____ _____ ____ ____ +// / \ \/ / _ \ / \ / \ / _ \ / \ +// \ \___( <_> ) Y Y \ Y Y ( <_> ) | \ +// \______ /\____/|__|_| /__|_| /\____/|___| / +// \/ \/ \/ \/ + +// This file provides common functions relating to GPG Keys + +// checkArmoredGPGKeyString checks if the given key string is a valid GPG armored key. +// The function returns the actual public key on success +func checkArmoredGPGKeyString(content string) (openpgp.EntityList, error) { + list, err := openpgp.ReadArmoredKeyRing(strings.NewReader(content)) + if err != nil { + return nil, ErrGPGKeyParsing{err} + } + return list, nil +} + +// base64EncPubKey encode public key content to base 64 +func base64EncPubKey(pubkey *packet.PublicKey) (string, error) { + var w bytes.Buffer + err := pubkey.Serialize(&w) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(w.Bytes()), nil +} + +func readerFromBase64(s string) (io.Reader, error) { + bs, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + return bytes.NewBuffer(bs), nil +} + +// base64DecPubKey decode public key content from base 64 +func base64DecPubKey(content string) (*packet.PublicKey, error) { + b, err := readerFromBase64(content) + if err != nil { + return nil, err + } + // Read key + p, err := packet.Read(b) + if err != nil { + return nil, err + } + // Check type + pkey, ok := p.(*packet.PublicKey) + if !ok { + return nil, fmt.Errorf("key is not a public key") + } + return pkey, nil +} + +// getExpiryTime extract the expire time of primary key based on sig +func getExpiryTime(e *openpgp.Entity) time.Time { + expiry := time.Time{} + // Extract self-sign for expire date based on : https://github.com/golang/crypto/blob/master/openpgp/keys.go#L165 + var selfSig *packet.Signature + for _, ident := range e.Identities { + if selfSig == nil { + selfSig = ident.SelfSignature + } else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId { + selfSig = ident.SelfSignature + break + } + } + if selfSig.KeyLifetimeSecs != nil { + expiry = e.PrimaryKey.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs) * time.Second) + } + return expiry +} + +func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) { + h := hashFunc.New() + if _, err := h.Write(msg); err != nil { + return nil, err + } + return h, nil +} + +// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17 +func readArmoredSign(r io.Reader) (body io.Reader, err error) { + block, err := armor.Decode(r) + if err != nil { + return + } + if block.Type != openpgp.SignatureType { + return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type) + } + return block.Body, nil +} + +func extractSignature(s string) (*packet.Signature, error) { + r, err := readArmoredSign(strings.NewReader(s)) + if err != nil { + return nil, fmt.Errorf("Failed to read signature armor") + } + p, err := packet.Read(r) + if err != nil { + return nil, fmt.Errorf("Failed to read signature packet") + } + sig, ok := p.(*packet.Signature) + if !ok { + return nil, fmt.Errorf("Packet is not a signature") + } + return sig, nil +} diff --git a/models/gpg_key_import.go b/models/gpg_key_import.go new file mode 100644 index 0000000000..bd1d530eca --- /dev/null +++ b/models/gpg_key_import.go @@ -0,0 +1,38 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package models + +// __________________ ________ ____ __. +// / _____/\______ \/ _____/ | |/ _|____ ___.__. +// / \ ___ | ___/ \ ___ | <_/ __ < | | +// \ \_\ \| | \ \_\ \ | | \ ___/\___ | +// \______ /|____| \______ / |____|__ \___ > ____| +// \/ \/ \/ \/\/ +// .___ __ +// | | _____ ______ ____________/ |_ +// | |/ \\____ \ / _ \_ __ \ __\ +// | | Y Y \ |_> > <_> ) | \/| | +// |___|__|_| / __/ \____/|__| |__| +// \/|__| + +// This file contains functions related to the original import of a key + +// GPGKeyImport the original import of key +type GPGKeyImport struct { + KeyID string `xorm:"pk CHAR(16) NOT NULL"` + Content string `xorm:"TEXT NOT NULL"` +} + +// GetGPGImportByKeyID returns the import public armored key by given KeyID. +func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) { + key := new(GPGKeyImport) + has, err := x.ID(keyID).Get(key) + if err != nil { + return nil, err + } else if !has { + return nil, ErrGPGKeyImportNotExist{keyID} + } + return key, nil +} diff --git a/models/gpg_key_test.go b/models/gpg_key_test.go index f04eb8eadb..be2d8a223b 100644 --- a/models/gpg_key_test.go +++ b/models/gpg_key_test.go @@ -227,7 +227,7 @@ Q0KHb+QcycSgbDx0ZAvdIacuKvBBcbxrsmFUI4LR+oIup0G9gUc0roPvr014jYQL =zHo9 -----END PGP PUBLIC KEY BLOCK-----` - keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters) + keys, err := AddGPGKey(1, testEmailWithUpperCaseLetters, "", "") assert.NoError(t, err) if assert.NotEmpty(t, keys) { key := keys[0] diff --git a/models/gpg_key_verify.go b/models/gpg_key_verify.go new file mode 100644 index 0000000000..15774dc058 --- /dev/null +++ b/models/gpg_key_verify.go @@ -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)) +} diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index de60d89bbe..7a4193199c 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -325,6 +325,8 @@ var migrations = []Migration{ NewMigration("Create protected tag table", createProtectedTagTable), // v187 -> v188 NewMigration("Drop unneeded webhook related columns", dropWebhookColumns), + // v188 -> v189 + NewMigration("Add key is verified to gpg key", addKeyIsVerified), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v188.go b/models/migrations/v188.go new file mode 100644 index 0000000000..52ef4aaa81 --- /dev/null +++ b/models/migrations/v188.go @@ -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)) +} |