Support GPG clearsign for verification #7

Merged
d merged 2 commits from support-gpg-signed-messaged into main 2026-06-14 19:25:32 +00:00
4 changed files with 110 additions and 30 deletions
+2 -9
View File
@@ -5,7 +5,6 @@ package asymkey
import (
"context"
"strings"
"gitea.dev/models/db"
"gitea.dev/modules/log"
@@ -78,15 +77,9 @@ func AddGPGKey(ctx context.Context, ownerID int64, content, token, signature str
verified := false
// Handle provided signature
if signature != "" {
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
signer, err := checkSignatureWithClearsign(ekeys, token, signature)
if err != nil {
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
}
if err != nil {
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
}
if err != nil {
log.Debug("AddGPGKey CheckArmoredDetachedSignature failed: %v", err)
log.Debug("AddGPGKey checkSignatureWithClearsign failed: %v", err)
return nil, ErrGPGInvalidTokenSignature{
ID: ekeys[0].PrimaryKey.KeyIdString(),
Wrapped: err,
+72 -3
View File
@@ -16,6 +16,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
@@ -121,21 +122,89 @@ func readArmoredSign(r io.Reader) (body io.Reader, err error) {
}
func ExtractSignature(s string) (*packet.Signature, error) {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "-----BEGIN PGP SIGNED MESSAGE-----") {
block, _ := clearsign.Decode([]byte(s))
if block == nil {
return nil, errors.New("failed to decode clearsign block")
}
p, err := packet.Read(block.ArmoredSignature.Body)
if err != nil {
return nil, errors.New("failed to read signature packet from clearsign")
}
sig, ok := p.(*packet.Signature)
if !ok {
return nil, errors.New("packet is not a signature")
}
return sig, nil
}
r, err := readArmoredSign(strings.NewReader(s))
if err != nil {
return nil, errors.New("Failed to read signature armor")
return nil, errors.New("failed to read signature armor")
}
p, err := packet.Read(r)
if err != nil {
return nil, errors.New("Failed to read signature packet")
return nil, errors.New("failed to read signature packet")
}
sig, ok := p.(*packet.Signature)
if !ok {
return nil, errors.New("Packet is not a signature")
return nil, errors.New("packet is not a signature")
}
return sig, nil
}
func extractSignatureAndPayload(signature string) (*packet.Signature, string, error) {
s := strings.TrimSpace(signature)
if strings.HasPrefix(s, "-----BEGIN PGP SIGNED MESSAGE-----") {
block, _ := clearsign.Decode([]byte(s))
if block == nil {
return nil, "", errors.New("failed to decode clearsign block")
}
p, err := packet.Read(block.ArmoredSignature.Body)
if err != nil {
return nil, "", errors.New("failed to read signature packet from clearsign")
}
sig, ok := p.(*packet.Signature)
if !ok {
return nil, "", errors.New("packet is not a signature")
}
return sig, string(block.Bytes), nil
}
sig, err := ExtractSignature(s)
if err != nil {
return nil, "", err
}
return sig, "", nil
}
func checkSignatureWithClearsign(ekeys openpgp.EntityList, token, signature string) (*openpgp.Entity, error) {
s := strings.TrimSpace(signature)
if strings.HasPrefix(s, "-----BEGIN PGP SIGNED MESSAGE-----") {
block, _ := clearsign.Decode([]byte(s))
if block == nil {
return nil, errors.New("failed to decode clearsign block")
}
if !strings.Contains(string(block.Bytes), token) {
return nil, errors.New("clearsign body does not contain token")
}
return openpgp.CheckDetachedSignature(ekeys, bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, nil)
}
signer, err := openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token), strings.NewReader(signature), nil)
if err != nil {
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\n"), strings.NewReader(signature), nil)
}
if err != nil {
signer, err = openpgp.CheckArmoredDetachedSignature(ekeys, strings.NewReader(token+"\r\n"), strings.NewReader(signature), nil)
}
return signer, err
}
func TryGetKeyIDFromSignature(sig *packet.Signature) string {
if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
return fmt.Sprintf("%016X", *sig.IssuerKeyId)
+32 -18
View File
@@ -7,6 +7,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"
"gitea.dev/models/db"
@@ -55,14 +56,12 @@ func VerifyNonce(nonce string) bool {
return true
}
// VerifyGPGSignature verifies a detached GPG signature against a nonce,
// extracts the key ID, looks up the key in the DB and returns the owning user.
func VerifyGPGSignature(ctx context.Context, nonce, signature string) (*user_model.User, error) {
if !VerifyNonce(nonce) {
return nil, ErrGPGInvalidTokenSignature{}
}
sig, err := ExtractSignature(signature)
sig, clearsignPayload, err := extractSignatureAndPayload(signature)
if err != nil {
return nil, ErrGPGInvalidTokenSignature{Wrapped: err}
}
@@ -84,12 +83,20 @@ func VerifyGPGSignature(ctx context.Context, nonce, signature string) (*user_mod
return nil, err
}
signer, err := hashAndVerifyWithSubKeys(sig, nonce, key)
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n", key)
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", key)
var signer *GPGKey
if clearsignPayload != "" {
if !strings.Contains(clearsignPayload, nonce) {
return nil, ErrGPGInvalidTokenSignature{}
}
signer, err = hashAndVerifyWithSubKeys(sig, clearsignPayload, key)
} else {
signer, err = hashAndVerifyWithSubKeys(sig, nonce, key)
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n", key)
}
if signer == nil {
signer, err = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", key)
}
}
if signer == nil {
return nil, ErrGPGInvalidTokenSignature{ID: key.KeyID, Wrapped: err}
@@ -103,18 +110,20 @@ func VerifyGPGSignature(ctx context.Context, nonce, signature string) (*user_mod
return user, nil
}
// VerifyGPGSignatureWithKey verifies a detached GPG signature against a nonce
// using a provided armored public key, without requiring it to be in the DB.
func VerifyGPGSignatureWithKey(nonce, signature, armoredKey string) (bool, error) {
if !VerifyNonce(nonce) {
return false, ErrGPGInvalidTokenSignature{}
}
sig, err := ExtractSignature(signature)
sig, clearsignPayload, err := extractSignatureAndPayload(signature)
if err != nil {
return false, ErrGPGInvalidTokenSignature{Wrapped: err}
}
if clearsignPayload != "" && !strings.Contains(clearsignPayload, nonce) {
return false, ErrGPGInvalidTokenSignature{}
}
keys, err := CheckArmoredGPGKeyString(armoredKey)
if err != nil {
return false, err
@@ -143,12 +152,17 @@ func VerifyGPGSignatureWithKey(nonce, signature, armoredKey string) (bool, error
})
}
signer, _ := hashAndVerifyWithSubKeys(sig, nonce, gpgKey)
if signer == nil {
signer, _ = hashAndVerifyWithSubKeys(sig, nonce+"\n", gpgKey)
}
if signer == nil {
signer, _ = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", gpgKey)
var signer *GPGKey
if clearsignPayload != "" {
signer, _ = hashAndVerifyWithSubKeys(sig, clearsignPayload, gpgKey)
} else {
signer, _ = hashAndVerifyWithSubKeys(sig, nonce, gpgKey)
if signer == nil {
signer, _ = hashAndVerifyWithSubKeys(sig, nonce+"\n", gpgKey)
}
if signer == nil {
signer, _ = hashAndVerifyWithSubKeys(sig, nonce+"\n\n", gpgKey)
}
}
if signer != nil {
return true, nil
+4
View File
@@ -359,4 +359,8 @@ func loadKeysData(ctx *context.Context) {
ctx.Data["VerifyingID"] = ctx.FormString("verify_gpg")
ctx.Data["VerifyingFingerprint"] = ctx.FormString("verify_ssh")
if fp := ctx.FormString("verify_ssh"); fp != "" {
ctx.Data["TokenToSign"] = asymkey_model.VerificationToken(ctx.Doer, 1)
}
}