| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- // Copyright 2025 The Gogs 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 gpgutil
- import (
- "bytes"
- "encoding/hex"
- "io"
- "strings"
- "github.com/pkg/errors"
- "golang.org/x/crypto/openpgp"
- "golang.org/x/crypto/openpgp/armor"
- "golang.org/x/crypto/openpgp/packet"
- )
- // CommitVerification represents the verification status of a Git commit signature.
- type CommitVerification struct {
- Verified bool // Whether the signature is valid
- Reason string // Reason for verification failure (if any)
- SignerKey string // Key ID or fingerprint of the signer
- SignerUID string // User ID (name + email) of the signer
- }
- // KeyInfo contains parsed information from a GPG public key.
- type KeyInfo struct {
- KeyID string // Short key ID (8 or 16 chars)
- Fingerprint string // Full 40-character fingerprint
- Emails []string // Email addresses in the key
- CanSign bool // Whether the key can sign
- CanEncrypt bool // Whether the key can encrypt
- ExpiredUnix int64 // Unix timestamp when key expires (0 if never)
- }
- // ParsePublicKey parses an ASCII-armored GPG public key and extracts key information.
- func ParsePublicKey(armoredKey string) (*KeyInfo, *openpgp.Entity, error) {
- reader := strings.NewReader(armoredKey)
- block, err := armor.Decode(reader)
- if err != nil {
- return nil, nil, errors.Wrap(err, "decode armored key")
- }
- keyReader := packet.NewReader(block.Body)
- entity, err := openpgp.ReadEntity(keyReader)
- if err != nil {
- return nil, nil, errors.Wrap(err, "read entity")
- }
- // Extract key ID and fingerprint
- fingerprint := hex.EncodeToString(entity.PrimaryKey.Fingerprint[:])
- keyID := strings.ToUpper(fingerprint[len(fingerprint)-16:]) // Last 16 chars (8 bytes)
- // Extract email addresses from identities
- var emails []string
- for _, identity := range entity.Identities {
- if identity.UserId != nil && identity.UserId.Email != "" {
- emails = append(emails, identity.UserId.Email)
- }
- }
- // Check key capabilities
- canSign := entity.PrimaryKey.PubKeyAlgo.CanSign()
- canEncrypt := entity.PrimaryKey.PubKeyAlgo.CanEncrypt()
- // Check expiration
- var expiredUnix int64
- if entity.PrimaryKey.KeyLifetimeSecs != nil && *entity.PrimaryKey.KeyLifetimeSecs > 0 {
- expiredUnix = entity.PrimaryKey.CreationTime.Unix() + int64(*entity.PrimaryKey.KeyLifetimeSecs)
- }
- info := &KeyInfo{
- KeyID: keyID,
- Fingerprint: strings.ToUpper(fingerprint),
- Emails: emails,
- CanSign: canSign,
- CanEncrypt: canEncrypt,
- ExpiredUnix: expiredUnix,
- }
- return info, entity, nil
- }
- // VerifyCommitSignature verifies a Git commit signature against a keyring.
- func VerifyCommitSignature(signature, content string, keyring openpgp.EntityList) (*CommitVerification, error) {
- // Decode the signature
- sigReader := strings.NewReader(signature)
- block, err := armor.Decode(sigReader)
- if err != nil {
- return &CommitVerification{
- Verified: false,
- Reason: "failed to decode signature",
- }, nil
- }
- // Verify the signature
- contentReader := strings.NewReader(content)
- signer, err := openpgp.CheckDetachedSignature(keyring, contentReader, block.Body, nil)
- if err != nil {
- return &CommitVerification{
- Verified: false,
- Reason: "signature verification failed: " + err.Error(),
- }, nil
- }
- // Extract signer information
- var signerUID string
- for _, identity := range signer.Identities {
- if identity.UserId != nil {
- signerUID = identity.UserId.Name
- if identity.UserId.Email != "" {
- signerUID += " <" + identity.UserId.Email + ">"
- }
- break
- }
- }
- fingerprint := hex.EncodeToString(signer.PrimaryKey.Fingerprint[:])
- keyID := strings.ToUpper(fingerprint[len(fingerprint)-16:])
- return &CommitVerification{
- Verified: true,
- Reason: "signature verified",
- SignerKey: keyID,
- SignerUID: signerUID,
- }, nil
- }
- // CreateKeyring creates an openpgp keyring from a list of ASCII-armored public keys.
- func CreateKeyring(armoredKeys []string) (openpgp.EntityList, error) {
- var keyring openpgp.EntityList
- for _, armoredKey := range armoredKeys {
- reader := strings.NewReader(armoredKey)
- block, err := armor.Decode(reader)
- if err != nil {
- continue // Skip invalid keys
- }
- entityList, err := openpgp.ReadKeyRing(block.Body)
- if err != nil {
- continue // Skip invalid keys
- }
- keyring = append(keyring, entityList...)
- }
- return keyring, nil
- }
- // ExtractSignature extracts the GPG signature from a Git commit object.
- // Git commit format includes an optional "gpgsig" header with the signature.
- func ExtractSignature(commitContent string) (signature string, payload string, hasSig bool) {
- // Look for the gpgsig header
- if !strings.Contains(commitContent, "gpgsig ") {
- return "", commitContent, false
- }
- lines := strings.Split(commitContent, "\n")
- var sigLines []string
- var payloadLines []string
- inSignature := false
- skipNext := false
- for i, line := range lines {
- if skipNext {
- skipNext = false
- continue
- }
- if strings.HasPrefix(line, "gpgsig ") {
- inSignature = true
- // First line of signature (without "gpgsig " prefix)
- sigLines = append(sigLines, strings.TrimPrefix(line, "gpgsig "))
- // Don't include this line in payload
- continue
- } else if inSignature && strings.HasPrefix(line, " ") {
- // Continuation of signature (remove leading space)
- sigLines = append(sigLines, strings.TrimPrefix(line, " "))
- continue
- } else if inSignature {
- // End of signature
- inSignature = false
- }
- // Add to payload
- payloadLines = append(payloadLines, line)
- // Reconstruct the commit without the gpgsig for verification
- if i == 0 || !strings.HasPrefix(lines[i-1], "gpgsig") {
- // This is a normal line
- }
- }
- signature = strings.Join(sigLines, "\n")
- payload = strings.Join(payloadLines, "\n")
- return signature, payload, len(sigLines) > 0
- }
- // SerializePublicKey converts an openpgp.Entity to an ASCII-armored string.
- func SerializePublicKey(entity *openpgp.Entity) (string, error) {
- var buf bytes.Buffer
- w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
- if err != nil {
- return "", errors.Wrap(err, "armor encode")
- }
- err = entity.Serialize(w)
- if err != nil {
- _ = w.Close()
- return "", errors.Wrap(err, "serialize entity")
- }
- err = w.Close()
- if err != nil {
- return "", errors.Wrap(err, "close armor writer")
- }
- return buf.String(), nil
- }
- // ValidatePublicKey checks if the provided string is a valid ASCII-armored GPG public key.
- func ValidatePublicKey(armoredKey string) error {
- _, _, err := ParsePublicKey(armoredKey)
- return err
- }
- // CreateEntityList creates an EntityList from a single armored key string.
- func CreateEntityListFromKey(armoredKey string) (openpgp.EntityList, error) {
- reader := strings.NewReader(armoredKey)
- block, err := armor.Decode(reader)
- if err != nil {
- return nil, errors.Wrap(err, "decode armored key")
- }
- entityList, err := openpgp.ReadKeyRing(block.Body)
- if err != nil {
- return nil, errors.Wrap(err, "read keyring")
- }
- return entityList, nil
- }
|