gpg.go 6.9 KB

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