v0.31.0 d5e0505

gpg.go RawHistoryFolder

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.
44func (GPG) Verify(payload, signature []byte, committerEmail string, _ int64, keys []domain.TrustedKey) (v domain.CommitVerification) {
45	v = domain.CommitVerification{Format: domain.FormatGPG, Status: domain.StatusBadSignature}
46	defer func() { // a parser bug must not take the server down
47		if recover() != nil {
48			v = domain.CommitVerification{Format: domain.FormatGPG, Status: domain.StatusBadSignature, Fingerprint: v.Fingerprint}
49		}
50	}()
51	if len(signature) > maxSignatureBytes {
52		return v
53	}
54	block, err := armor.Decode(bytes.NewReader(signature))
55	if err != nil || block.Type != openpgp.SignatureType {
56		return v
57	}
58	body, err := io.ReadAll(io.LimitReader(block.Body, maxSignatureBytes))
59	if err != nil {
60		return v
61	}
62	sig, ok := onlySignature(body)
63	if !ok {
64		return v
65	}
66	issuer := *sig.IssuerKeyId
67	v.Fingerprint = fmt.Sprintf("%016X", issuer)
68
69	ring, byFingerprint := gpgKeyring(keys)
70	created := sig.CreationTime
71	cfg := &packet.Config{Time: func() time.Time { return created }}
72	_, signer, err := openpgp.VerifyDetachedSignatureAndHash(ring, bytes.NewReader(payload), bytes.NewReader(body), acceptedHashes, cfg)
73	if signer == nil && !errors.Is(err, pgperrors.ErrUnknownIssuer) {
74		// The check failed before naming the signer: look it up for the fingerprint.
75		if known := ring.KeysById(issuer); len(known) > 0 {
76			signer = known[0].Entity
77		}
78	}
79	if signer != nil {
80		v.Fingerprint = fingerprintHex(signer)
81	}
82
83	if signer != nil && (err == nil || errors.Is(err, pgperrors.ErrKeyExpired) || errors.Is(err, pgperrors.ErrSignatureExpired)) &&
84		revokedNow(ring.KeysById(issuer), signer) {
85		// The library judged revocation at the signature date: a revocation
86		// published since is only seen with the current time.
87		v.Status = domain.StatusRevoked
88		return v
89	}
90
91	switch {
92	case err == nil:
93	case errors.Is(err, pgperrors.ErrUnknownIssuer):
94		v.Status = domain.StatusUnknownKey
95		return v
96	case errors.Is(err, pgperrors.ErrKeyExpired), errors.Is(err, pgperrors.ErrSignatureExpired):
97		v.Status = domain.StatusExpired
98		return v
99	case errors.Is(err, pgperrors.ErrKeyRevoked):
100		v.Status = domain.StatusRevoked
101		return v
102	default:
103		return v
104	}
105	if key, ok := byFingerprint[v.Fingerprint]; ok {
106		return trusted(v, key, committerEmail)
107	}
108	v.Status = domain.StatusUnknownKey
109	return v
110}
111
112// onlySignature returns the signature packet of body when body holds exactly
113// one packet, a signature with an issuer key id and a creation time. One
114// packet only: the library verifies the first packet whose issuer it knows,
115// so with several the clock and the revocation check could be taken from a
116// packet other than the one that verified.
117func onlySignature(body []byte) (*packet.Signature, bool) {
118	r := bytes.NewReader(body)
119	p, err := packet.Read(r)
120	if err != nil {
121		return nil, false
122	}
123	if _, err := packet.Read(r); !errors.Is(err, io.EOF) {
124		return nil, false
125	}
126	sig, ok := p.(*packet.Signature)
127	if !ok || sig.IssuerKeyId == nil || sig.CreationTime.IsZero() {
128		return nil, false
129	}
130	return sig, true
131}
132
133// revokedNow reports whether the entity, the signing key among keys (the
134// primary key or a subkey) or the primary user ID is revoked at the current
135// time.
136func revokedNow(keys []openpgp.Key, signer *openpgp.Entity) bool {
137	now := time.Now()
138	if signer.Revoked(now) {
139		return true
140	}
141	if _, id := signer.PrimarySelfSignature(); id != nil && id.Revoked(now) {
142		return true
143	}
144	for _, k := range keys {
145		if k.Entity == signer && k.Revoked(now) {
146			return true
147		}
148	}
149	return false
150}
151
152// gpgKeyring builds the keyring of the trusted GPG keys, and indexes the
153// trusted keys by the primary fingerprint. Unreadable keys and private keys
154// are skipped.
155func gpgKeyring(keys []domain.TrustedKey) (openpgp.EntityList, map[string]domain.TrustedKey) {
156	var ring openpgp.EntityList
157	byFingerprint := make(map[string]domain.TrustedKey, len(keys))
158	for _, k := range keys {
159		if k.Format != domain.FormatGPG || len(k.PublicKey) > domain.MaxTrustedKeyBytes {
160			continue
161		}
162		entities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(k.PublicKey))
163		if err != nil {
164			continue
165		}
166		for _, e := range entities {
167			if hasPrivateKey(e) {
168				continue
169			}
170			ring = append(ring, e)
171			byFingerprint[fingerprintHex(e)] = k
172		}
173	}
174	return ring, byFingerprint
175}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 45

↑↓ moveenter openesc close