gpg.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. // Copyright 2025 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package gpgutil
  5. import (
  6. "bytes"
  7. "encoding/hex"
  8. "io"
  9. "strings"
  10. "github.com/pkg/errors"
  11. "golang.org/x/crypto/openpgp"
  12. "golang.org/x/crypto/openpgp/armor"
  13. "golang.org/x/crypto/openpgp/packet"
  14. )
  15. // CommitVerification represents the verification status of a Git commit signature.
  16. type CommitVerification struct {
  17. Verified bool // Whether the signature is valid
  18. Reason string // Reason for verification failure (if any)
  19. SignerKey string // Key ID or fingerprint of the signer
  20. SignerUID string // User ID (name + email) of the signer
  21. }
  22. // KeyInfo contains parsed information from a GPG public key.
  23. type KeyInfo struct {
  24. KeyID string // Short key ID (8 or 16 chars)
  25. Fingerprint string // Full 40-character fingerprint
  26. Emails []string // Email addresses in the key
  27. CanSign bool // Whether the key can sign
  28. CanEncrypt bool // Whether the key can encrypt
  29. ExpiredUnix int64 // Unix timestamp when key expires (0 if never)
  30. }
  31. // ParsePublicKey parses an ASCII-armored GPG public key and extracts key information.
  32. func ParsePublicKey(armoredKey string) (*KeyInfo, *openpgp.Entity, error) {
  33. reader := strings.NewReader(armoredKey)
  34. block, err := armor.Decode(reader)
  35. if err != nil {
  36. return nil, nil, errors.Wrap(err, "decode armored key")
  37. }
  38. keyReader := packet.NewReader(block.Body)
  39. entity, err := openpgp.ReadEntity(keyReader)
  40. if err != nil {
  41. return nil, nil, errors.Wrap(err, "read entity")
  42. }
  43. // Extract key ID and fingerprint
  44. fingerprint := hex.EncodeToString(entity.PrimaryKey.Fingerprint[:])
  45. keyID := strings.ToUpper(fingerprint[len(fingerprint)-16:]) // Last 16 chars (8 bytes)
  46. // Extract email addresses from identities
  47. var emails []string
  48. for _, identity := range entity.Identities {
  49. if identity.UserId != nil && identity.UserId.Email != "" {
  50. emails = append(emails, identity.UserId.Email)
  51. }
  52. }
  53. // Check key capabilities
  54. canSign := entity.PrimaryKey.PubKeyAlgo.CanSign()
  55. canEncrypt := entity.PrimaryKey.PubKeyAlgo.CanEncrypt()
  56. // Check expiration
  57. var expiredUnix int64
  58. if entity.PrimaryKey.KeyLifetimeSecs != nil && *entity.PrimaryKey.KeyLifetimeSecs > 0 {
  59. expiredUnix = entity.PrimaryKey.CreationTime.Unix() + int64(*entity.PrimaryKey.KeyLifetimeSecs)
  60. }
  61. info := &KeyInfo{
  62. KeyID: keyID,
  63. Fingerprint: strings.ToUpper(fingerprint),
  64. Emails: emails,
  65. CanSign: canSign,
  66. CanEncrypt: canEncrypt,
  67. ExpiredUnix: expiredUnix,
  68. }
  69. return info, entity, nil
  70. }
  71. // VerifyCommitSignature verifies a Git commit signature against a keyring.
  72. func VerifyCommitSignature(signature, content string, keyring openpgp.EntityList) (*CommitVerification, error) {
  73. // Decode the signature
  74. sigReader := strings.NewReader(signature)
  75. block, err := armor.Decode(sigReader)
  76. if err != nil {
  77. return &CommitVerification{
  78. Verified: false,
  79. Reason: "failed to decode signature",
  80. }, nil
  81. }
  82. // Verify the signature
  83. contentReader := strings.NewReader(content)
  84. signer, err := openpgp.CheckDetachedSignature(keyring, contentReader, block.Body, nil)
  85. if err != nil {
  86. return &CommitVerification{
  87. Verified: false,
  88. Reason: "signature verification failed: " + err.Error(),
  89. }, nil
  90. }
  91. // Extract signer information
  92. var signerUID string
  93. for _, identity := range signer.Identities {
  94. if identity.UserId != nil {
  95. signerUID = identity.UserId.Name
  96. if identity.UserId.Email != "" {
  97. signerUID += " <" + identity.UserId.Email + ">"
  98. }
  99. break
  100. }
  101. }
  102. fingerprint := hex.EncodeToString(signer.PrimaryKey.Fingerprint[:])
  103. keyID := strings.ToUpper(fingerprint[len(fingerprint)-16:])
  104. return &CommitVerification{
  105. Verified: true,
  106. Reason: "signature verified",
  107. SignerKey: keyID,
  108. SignerUID: signerUID,
  109. }, nil
  110. }
  111. // CreateKeyring creates an openpgp keyring from a list of ASCII-armored public keys.
  112. func CreateKeyring(armoredKeys []string) (openpgp.EntityList, error) {
  113. var keyring openpgp.EntityList
  114. for _, armoredKey := range armoredKeys {
  115. reader := strings.NewReader(armoredKey)
  116. block, err := armor.Decode(reader)
  117. if err != nil {
  118. continue // Skip invalid keys
  119. }
  120. entityList, err := openpgp.ReadKeyRing(block.Body)
  121. if err != nil {
  122. continue // Skip invalid keys
  123. }
  124. keyring = append(keyring, entityList...)
  125. }
  126. return keyring, nil
  127. }
  128. // ExtractSignature extracts the GPG signature from a Git commit object.
  129. // Git commit format includes an optional "gpgsig" header with the signature.
  130. func ExtractSignature(commitContent string) (signature string, payload string, hasSig bool) {
  131. // Look for the gpgsig header
  132. if !strings.Contains(commitContent, "gpgsig ") {
  133. return "", commitContent, false
  134. }
  135. lines := strings.Split(commitContent, "\n")
  136. var sigLines []string
  137. var payloadLines []string
  138. inSignature := false
  139. skipNext := false
  140. for i, line := range lines {
  141. if skipNext {
  142. skipNext = false
  143. continue
  144. }
  145. if strings.HasPrefix(line, "gpgsig ") {
  146. inSignature = true
  147. // First line of signature (without "gpgsig " prefix)
  148. sigLines = append(sigLines, strings.TrimPrefix(line, "gpgsig "))
  149. // Don't include this line in payload
  150. continue
  151. } else if inSignature && strings.HasPrefix(line, " ") {
  152. // Continuation of signature (remove leading space)
  153. sigLines = append(sigLines, strings.TrimPrefix(line, " "))
  154. continue
  155. } else if inSignature {
  156. // End of signature
  157. inSignature = false
  158. }
  159. // Add to payload
  160. payloadLines = append(payloadLines, line)
  161. // Reconstruct the commit without the gpgsig for verification
  162. if i == 0 || !strings.HasPrefix(lines[i-1], "gpgsig") {
  163. // This is a normal line
  164. }
  165. }
  166. signature = strings.Join(sigLines, "\n")
  167. payload = strings.Join(payloadLines, "\n")
  168. return signature, payload, len(sigLines) > 0
  169. }
  170. // SerializePublicKey converts an openpgp.Entity to an ASCII-armored string.
  171. func SerializePublicKey(entity *openpgp.Entity) (string, error) {
  172. var buf bytes.Buffer
  173. w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil)
  174. if err != nil {
  175. return "", errors.Wrap(err, "armor encode")
  176. }
  177. err = entity.Serialize(w)
  178. if err != nil {
  179. _ = w.Close()
  180. return "", errors.Wrap(err, "serialize entity")
  181. }
  182. err = w.Close()
  183. if err != nil {
  184. return "", errors.Wrap(err, "close armor writer")
  185. }
  186. return buf.String(), nil
  187. }
  188. // ValidatePublicKey checks if the provided string is a valid ASCII-armored GPG public key.
  189. func ValidatePublicKey(armoredKey string) error {
  190. _, _, err := ParsePublicKey(armoredKey)
  191. return err
  192. }
  193. // CreateEntityList creates an EntityList from a single armored key string.
  194. func CreateEntityListFromKey(armoredKey string) (openpgp.EntityList, error) {
  195. reader := strings.NewReader(armoredKey)
  196. block, err := armor.Decode(reader)
  197. if err != nil {
  198. return nil, errors.Wrap(err, "decode armored key")
  199. }
  200. entityList, err := openpgp.ReadKeyRing(block.Body)
  201. if err != nil {
  202. return nil, errors.Wrap(err, "read keyring")
  203. }
  204. return entityList, nil
  205. }