Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ssh commit signing #17743

Merged
merged 27 commits into from
Dec 19, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8d38285
Add support for ssh commit signing
42wim Nov 21, 2021
9b1182b
Split out ssh verification to separate file
42wim Nov 21, 2021
9f113c3
Show ssh key fingerprint on commit page
42wim Nov 21, 2021
af5fc14
Update sshsig lib
42wim Nov 21, 2021
7b6b1a1
Make sure we verify against correct namespace
42wim Nov 21, 2021
741ad05
Add ssh public key verification via ssh signatures
42wim Nov 21, 2021
bea996d
Remove some gpg references and make verify key optional
42wim Nov 21, 2021
5439269
Fix spaces indentation
42wim Nov 21, 2021
2d0bdf0
Update options/locale/locale_en-US.ini
42wim Nov 22, 2021
fd91370
Update templates/user/settings/keys_ssh.tmpl
42wim Nov 22, 2021
543c972
Update options/locale/locale_en-US.ini
42wim Nov 22, 2021
4ab202f
Update options/locale/locale_en-US.ini
42wim Nov 22, 2021
1172df7
Update models/ssh_key_commit_verification.go
42wim Nov 22, 2021
496b0aa
Reword ssh/gpg_key_success message
42wim Nov 22, 2021
d216536
Merge branch 'main' into ssh-sign
42wim Nov 22, 2021
b02effc
Change Badsignature to NoKeyFound
42wim Nov 22, 2021
378f119
Merge branch 'main' into ssh-sign
42wim Nov 27, 2021
670cc92
Add sign/verify tests
42wim Dec 1, 2021
2ccc73d
Merge branch 'main' into ssh-sign
42wim Dec 1, 2021
fcf0178
Fix upstream api changes to user_model User
42wim Dec 1, 2021
aa5074b
Merge branch 'main' into ssh-sign
wxiaoguang Dec 3, 2021
3b406a3
Match exact on SSH signature
42wim Dec 3, 2021
bf66261
Merge branch 'main' into ssh-sign
42wim Dec 12, 2021
72a8d09
Fix code review remarks
42wim Dec 12, 2021
4b22267
Merge branch 'main' into ssh-sign
42wim Dec 19, 2021
882a980
Merge branch 'main' into ssh-sign
lunny Dec 19, 2021
9da693f
Merge branch 'main' into ssh-sign
techknowlogick Dec 19, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
gitea.com/go-chi/captcha v0.0.0-20211013065431-70641c1a35d5
gitea.com/go-chi/session v0.0.0-20211013065435-7d334f340c09
gitea.com/lunny/levelqueue v0.4.1
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121
github.com/Microsoft/go-winio v0.5.0 // indirect
github.com/NYTimes/gziphandler v1.1.1
github.com/ProtonMail/go-crypto v0.0.0-20210705153151-cc34b1f6908b // indirect
Expand Down Expand Up @@ -122,10 +123,10 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.19.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1
golang.org/x/text v0.3.7
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6 // indirect
golang.org/x/tools v0.1.0
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ gitea.com/lunny/levelqueue v0.4.1 h1:RZ+AFx5gBsZuyqCvofhAkPQ9uaVDPJnsULoJZIYaJNw
gitea.com/lunny/levelqueue v0.4.1/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s=
gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU=
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 h1:r3qt8PCHnfjOv9PN3H+XXKmDA1dfFMIN1AislhlA/ps=
github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121/go.mod h1:Ock8XgA7pvULhIaHGAk/cDnRfNrF9Jey81nPcc403iU=
github.com/6543/go-version v1.3.1 h1:HvOp+Telns7HWJ2Xo/05YXQSB2bE0WmVgbHqwMPZT4U=
github.com/6543/go-version v1.3.1/go.mod h1:oqFAHCwtLVUTLdhQmVZWYvaHXTdsbB4SY85at64SQEo=
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
Expand Down Expand Up @@ -1263,6 +1265,8 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -1357,6 +1361,8 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -1464,6 +1470,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k=
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
Expand Down
16 changes: 16 additions & 0 deletions models/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,22 @@ func (err ErrKeyNameAlreadyUsed) Error() string {
return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
}

// ErrSSHInvalidTokenSignature represents a "ErrSSHInvalidTokenSignature" kind of error.
type ErrSSHInvalidTokenSignature struct {
Wrapped error
Fingerprint string
}

// IsErrSSHInvalidTokenSignature checks if an error is a ErrSSHInvalidTokenSignature.
func IsErrSSHInvalidTokenSignature(err error) bool {
_, ok := err.(ErrSSHInvalidTokenSignature)
return ok
}

func (err ErrSSHInvalidTokenSignature) Error() string {
return "the provided signature does not sign the token with the provided key"
}

// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
type ErrGPGNoEmailFound struct {
FailedEmails []string
Expand Down
43 changes: 26 additions & 17 deletions models/gpg_key_commit_verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type CommitVerification struct {
CommittingUser *User
SigningEmail string
SigningKey *GPGKey
SigningSSHKey *PublicKey
TrustStatus string
}

Expand Down Expand Up @@ -121,6 +122,11 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
}
}

// If this a SSH signature handle it differently
if strings.Contains(c.Signature.Signature, "SSH") {
42wim marked this conversation as resolved.
Show resolved Hide resolved
return ParseCommitWithSSHSignature(c, committer)
}

// Parsing signature
sig, err := extractSignature(c.Signature.Signature)
if err != nil { // Skipping failed to extract sign
Expand Down Expand Up @@ -488,28 +494,31 @@ func CalculateTrustStatus(verification *CommitVerification, repository *Reposito
return
}

var isMember bool
if keyMap != nil {
var has bool
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
if !has {
// Check we actually have a GPG SigningKey
if verification.SigningKey != nil {
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)
(*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.
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"
}
} 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
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ var migrations = []Migration{
NewMigration("Add table app_state", addTableAppState),
// v201 -> v202
NewMigration("Drop table remote_version (if exists)", dropTableRemoteVersion),
// v202 -> v203
NewMigration("Add key is verified to ssh key", addSSHKeyIsVerified),
}

// GetCurrentDBVersion returns the current db version
Expand Down
15 changes: 15 additions & 0 deletions models/migrations/v202.go
Original file line number Diff line number Diff line change
@@ -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 addSSHKeyIsVerified(x *xorm.Engine) error {
type PublicKey struct {
Verified bool `xorm:"NOT NULL DEFAULT false"`
}

return x.Sync(new(PublicKey))
}
1 change: 1 addition & 0 deletions models/ssh_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type PublicKey struct {
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
HasRecentActivity bool `xorm:"-"`
HasUsed bool `xorm:"-"`
Verified bool `xorm:"NOT NULL DEFAULT false"`
}

func init() {
Expand Down
81 changes: 81 additions & 0 deletions models/ssh_key_commit_verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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"
"fmt"
"strings"

"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"github.com/42wim/sshsig"
)

// ParseCommitWithSSHSignature check if signature is good against keystore.
func ParseCommitWithSSHSignature(c *git.Commit, committer *User) *CommitVerification {
42wim marked this conversation as resolved.
Show resolved Hide resolved
// Now try to associate the signature with the committer, if present
if committer.ID != 0 {
keys, err := ListPublicKeys(committer.ID, db.ListOptions{})
if err != nil { // Skipping failed to get ssh keys of user
log.Error("ListPublicKeys: %v", err)
return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}

committerEmailAddresses, _ := user_model.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 {
canValidate := false
email := ""
if k.Verified && activated {
canValidate = true
email = c.Committer.Email
}

if !canValidate {
continue // Skip this key
}

commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, email)
if commitVerification != nil {
return commitVerification
}
42wim marked this conversation as resolved.
Show resolved Hide resolved
}
}

return &CommitVerification{
CommittingUser: committer,
Verified: false,
Reason: BadSignature,
42wim marked this conversation as resolved.
Show resolved Hide resolved
}
}

func verifySSHCommitVerification(sig, payload string, k *PublicKey, committer, signer *User, email string) *CommitVerification {
if err := sshsig.Verify(bytes.NewBuffer([]byte(payload)), []byte(sig), []byte(k.Content), "git"); err != nil {
return nil
}

return &CommitVerification{ // Everything is ok
CommittingUser: committer,
Verified: true,
Reason: fmt.Sprintf("%s / %s", signer.Name, k.Fingerprint),
SigningUser: signer,
SigningSSHKey: k,
SigningEmail: email,
}
}
49 changes: 49 additions & 0 deletions models/ssh_key_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// 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"

"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/log"
"github.com/42wim/sshsig"
)

// VerifySSHKey marks a SSH key as verified
func VerifySSHKey(ownerID int64, fingerprint, token, signature string) (string, error) {
ctx, committer, err := db.TxContext()
if err != nil {
return "", err
}
defer committer.Close()

key := new(PublicKey)

has, err := db.GetEngine(ctx).Where("owner_id = ? AND fingerprint = ?", ownerID, fingerprint).Get(key)
if err != nil {
return "", err
} else if !has {
return "", ErrKeyNotExist{}
}

if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil {
log.Error("Unable to validate token signature. Error: %v", err)
return "", ErrSSHInvalidTokenSignature{
Fingerprint: key.Fingerprint,
}
}

key.Verified = true
if _, err := db.GetEngine(ctx).ID(key.ID).SetExpr("verified", true).Update(new(PublicKey)); err != nil {
return "", err
}

if err := committer.Commit(); err != nil {
return "", err
}

return key.Fingerprint, nil
}
12 changes: 12 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,17 @@ 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.
ssh_key_verified=Verified Key
ssh_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.
ssh_key_verify=Verify
ssh_invalid_token_signature = The provided SSH key, signature and token do not match or token is out-of-date.
42wim marked this conversation as resolved.
Show resolved Hide resolved
ssh_token_required = You must provide a signature for the below token
ssh_token = Token
ssh_token_help = You can generate a signature using:
ssh_token_code = echo -n "%s" |ssh-keygen -Y sign -n gitea -f /path_to_your_pubkey
42wim marked this conversation as resolved.
Show resolved Hide resolved
gpg_token_signature = Armored SSH signature
42wim marked this conversation as resolved.
Show resolved Hide resolved
key_signature_ssh_placeholder = Begins with '-----BEGIN SSH SIGNATURE-----'
verify_ssh_key_success = The SSH key '%s' has been verified.
42wim marked this conversation as resolved.
Show resolved Hide resolved
subkeys = Subkeys
key_id = Key ID
key_name = Key Name
Expand Down Expand Up @@ -1071,6 +1082,7 @@ commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
commits.gpg_key_id = GPG Key ID
commits.ssh_key_fingerprint = SSH Key Fingerprint

ext_issues = Ext. Issues
ext_issues.desc = Link to an external issue tracker.
Expand Down
23 changes: 23 additions & 0 deletions routers/web/user/setting/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,28 @@ func KeysPost(ctx *context.Context) {
}
ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
case "verify_ssh":
token := models.VerificationToken(ctx.User, 1)
lastToken := models.VerificationToken(ctx.User, 0)

fingerprint, err := models.VerifySSHKey(ctx.User.ID, form.Fingerprint, token, form.Signature)
if err != nil && models.IsErrSSHInvalidTokenSignature(err) {
fingerprint, err = models.VerifySSHKey(ctx.User.ID, form.Fingerprint, lastToken, form.Signature)
}
if err != nil {
ctx.Data["HasSSHVerifyError"] = true
switch {
case models.IsErrSSHInvalidTokenSignature(err):
loadKeysData(ctx)
ctx.Data["Err_Signature"] = true
ctx.Data["Fingerprint"] = err.(models.ErrSSHInvalidTokenSignature).Fingerprint
ctx.RenderWithErr(ctx.Tr("settings.ssh_invalid_token_signature"), tplSettingsKeys, &form)
default:
ctx.ServerError("VerifySSH", err)
}
}
ctx.Flash.Success(ctx.Tr("settings.verify_ssh_key_success", fingerprint))
ctx.Redirect(setting.AppSubURL + "/user/settings/keys")

default:
ctx.Flash.Warning("Function not implemented")
Expand Down Expand Up @@ -267,4 +289,5 @@ func loadKeysData(ctx *context.Context) {
ctx.Data["Principals"] = principals

ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
}
13 changes: 7 additions & 6 deletions services/forms/user_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,13 @@ func (f *AddOpenIDForm) Validate(req *http.Request, errs binding.Errors) binding

// AddKeyForm form for adding SSH/GPG key
type AddKeyForm struct {
Type string `binding:"OmitEmpty"`
Title string `binding:"Required;MaxSize(50)"`
Content string `binding:"Required"`
Signature string `binding:"OmitEmpty"`
KeyID string `binding:"OmitEmpty"`
IsWritable bool
Type string `binding:"OmitEmpty"`
Title string `binding:"Required;MaxSize(50)"`
Content string `binding:"Required"`
Signature string `binding:"OmitEmpty"`
KeyID string `binding:"OmitEmpty"`
Fingerprint string `binding:"OmitEmpty"`
IsWritable bool
}

// Validate validates the fields
Expand Down
Loading