1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package sigverify
4
5import (
6 "bytes"
7 "crypto"
8 "errors"
9 "fmt"
10 "io"
11 "strings"
12 "time"
13
14 "github.com/ProtonMail/go-crypto/openpgp"
15 "github.com/ProtonMail/go-crypto/openpgp/armor"
16 pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
17 "github.com/ProtonMail/go-crypto/openpgp/packet"
18
19 "bnema.dev/ermite/internal/domain"
20 "bnema.dev/ermite/internal/ports"
21)
22
23// acceptedHashes are the digest algorithms of the signatures GPG accepts. The
24// library's own reject lists (packet.Config.RejectMessageHashAlgorithms) are
25// not applied to detached signatures, so the allowlist is enforced explicitly:
26// a signature over MD5, SHA-1, RIPEMD-160 or SHA-224 is a bad signature.
27var acceptedHashes = []crypto.Hash{crypto.SHA256, crypto.SHA384, crypto.SHA512, crypto.SHA3_256, crypto.SHA3_512}
28
29// GPG verifies OpenPGP detached signatures. It ignores the trusted keys that
30// are not GPG keys.
31//
32// Time policy: the committer date is not used. Key, subkey and signature
33// expiry are judged at the creation time of the signature, which the
34// signature authenticates, so a key that has expired since still verifies
35// what it signed while it was valid. Revocation is judged now: a signature by
36// a key that is revoked today (key, signing subkey or primary user ID) is
37// reported as revoked, whenever it was made.
38type GPG struct{}
39
40var _ ports.SignatureVerifier = GPG{}
41
42// Verify implements ports.SignatureVerifier. signedAt is ignored: see the time
43// policy of GPG.
44//
45//hexcheck:ignore suspicious-business-logic-in-adapter this is the OpenPGP protocol check the port delegates to the adapter; the trust and email rules stay in domain
46func (GPG) Verify(payload, signature []byte, committerEmail string, _ int64, keys []domain.TrustedKey) (v domain.CommitVerification) {
47 v = domain.CommitVerification{Format: domain.FormatGPG, Status: domain.StatusBadSignature}
48 defer func() { // a parser bug must not take the server down
49 if recover() != nil {
50 v = domain.CommitVerification{Format: domain.FormatGPG, Status: domain.StatusBadSignature, Fingerprint: v.Fingerprint}
51 }
52 }()
53 if len(signature) > maxSignatureBytes {
54 return v
55 }
56 block, err := armor.Decode(bytes.NewReader(signature))
57 if err != nil || block.Type != openpgp.SignatureType {
58 return v
59 }
60 body, err := io.ReadAll(io.LimitReader(block.Body, maxSignatureBytes))
61 if err != nil {
62 return v
63 }
64 sig, ok := onlySignature(body)
65 if !ok {
66 return v
67 }
68 issuer := *sig.IssuerKeyId
69 v.Fingerprint = fmt.Sprintf("%016X", issuer)
70
71 ring, byFingerprint := gpgKeyring(keys)
72 created := sig.CreationTime
73 cfg := &packet.Config{Time: func() time.Time { return created }}
74 _, signer, err := openpgp.VerifyDetachedSignatureAndHash(ring, bytes.NewReader(payload), bytes.NewReader(body), acceptedHashes, cfg)
75 if signer == nil && !errors.Is(err, pgperrors.ErrUnknownIssuer) {
76 // The check failed before naming the signer: look it up for the fingerprint.
77 if known := ring.KeysById(issuer); len(known) > 0 {
78 signer = known[0].Entity
79 }
80 }
81 if signer != nil {
82 v.Fingerprint = fingerprintHex(signer)
83 }
84
85 if signer != nil && (err == nil || errors.Is(err, pgperrors.ErrKeyExpired) || errors.Is(err, pgperrors.ErrSignatureExpired)) &&
86 revokedNow(ring.KeysById(issuer), signer) {
87 // The library judged revocation at the signature date: a revocation
88 // published since is only seen with the current time.
89 v.Status = domain.StatusRevoked
90 return v
91 }
92
93 switch {
94 case err == nil:
95 case errors.Is(err, pgperrors.ErrUnknownIssuer):
96 v.Status = domain.StatusUnknownKey
97 return v
98 case errors.Is(err, pgperrors.ErrKeyExpired), errors.Is(err, pgperrors.ErrSignatureExpired):
99 v.Status = domain.StatusExpired
100 return v
101 case errors.Is(err, pgperrors.ErrKeyRevoked):
102 v.Status = domain.StatusRevoked
103 return v
104 default:
105 return v
106 }
107 if key, ok := byFingerprint[v.Fingerprint]; ok {
108 return trusted(v, key, committerEmail)
109 }
110 v.Status = domain.StatusUnknownKey
111 return v
112}
113
114// onlySignature returns the signature packet of body when body holds exactly
115// one packet, a signature with an issuer key id and a creation time. One
116// packet only: the library verifies the first packet whose issuer it knows,
117// so with several the clock and the revocation check could be taken from a
118// packet other than the one that verified.
119func onlySignature(body []byte) (*packet.Signature, bool) {
120 r := bytes.NewReader(body)
121 p, err := packet.Read(r)
122 if err != nil {
123 return nil, false
124 }
125 if _, err := packet.Read(r); !errors.Is(err, io.EOF) {
126 return nil, false
127 }
128 sig, ok := p.(*packet.Signature)
129 if !ok || sig.IssuerKeyId == nil || sig.CreationTime.IsZero() {
130 return nil, false
131 }
132 return sig, true
133}
134
135// revokedNow reports whether the entity, the signing key among keys (the
136// primary key or a subkey) or the primary user ID is revoked at the current
137// time.
138func revokedNow(keys []openpgp.Key, signer *openpgp.Entity) bool {
139 now := time.Now()
140 if signer.Revoked(now) {
141 return true
142 }
143 if _, id := signer.PrimarySelfSignature(); id != nil && id.Revoked(now) {
144 return true
145 }
146 for _, k := range keys {
147 if k.Entity == signer && k.Revoked(now) {
148 return true
149 }
150 }
151 return false
152}
153
154// gpgKeyring builds the keyring of the trusted GPG keys, and indexes the
155// trusted keys by the primary fingerprint. Unreadable keys and private keys
156// are skipped.
157func gpgKeyring(keys []domain.TrustedKey) (openpgp.EntityList, map[string]domain.TrustedKey) {
158 var ring openpgp.EntityList
159 byFingerprint := make(map[string]domain.TrustedKey, len(keys))
160 for _, k := range keys {
161 if k.Format != domain.FormatGPG || len(k.PublicKey) > domain.MaxTrustedKeyBytes {
162 continue
163 }
164 entities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(k.PublicKey))
165 if err != nil {
166 continue
167 }
168 for _, e := range entities {
169 if hasPrivateKey(e) {
170 continue
171 }
172 ring = append(ring, e)
173 byFingerprint[fingerprintHex(e)] = k
174 }
175 }
176 return ring, byFingerprint
177}