detached cab7cdc

inspector.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package sigverify
4
5import (
6	"bytes"
7	"encoding/hex"
8	"errors"
9	"fmt"
10	"strings"
11
12	"github.com/ProtonMail/go-crypto/openpgp"
13	"github.com/ProtonMail/go-crypto/openpgp/armor"
14	"golang.org/x/crypto/ssh"
15
16	"bnema.dev/ermite/internal/domain"
17	"bnema.dev/ermite/internal/ports"
18)
19
20// Inspector parses the public keys to trust. The zero value is ready.
21type Inspector struct{}
22
23var _ ports.KeyInspector = Inspector{}
24
25// Inspect implements ports.KeyInspector.
26func (Inspector) Inspect(format domain.SignatureFormat, publicKey string) (id, canonical string, err error) {
27	if len(publicKey) > domain.MaxTrustedKeyBytes {
28		return "", "", fmt.Errorf("%w: public key larger than %d bytes", domain.ErrInvalid, domain.MaxTrustedKeyBytes)
29	}
30	defer func() { // a parser bug must not take the server down
31		if recover() != nil {
32			id, canonical, err = "", "", fmt.Errorf("%w: unreadable public key", domain.ErrInvalid)
33		}
34	}()
35	switch format {
36	case domain.FormatSSH:
37		return inspectSSH(publicKey)
38	case domain.FormatGPG:
39		return inspectGPG(publicKey)
40	}
41	return "", "", fmt.Errorf("%w: key format %q, want ssh or gpg", domain.ErrInvalid, format)
42}
43
44func inspectSSH(publicKey string) (id, canonical string, err error) {
45	pk, _, _, rest, err := ssh.ParseAuthorizedKey([]byte(publicKey))
46	if err != nil {
47		return "", "", fmt.Errorf("%w: not an SSH public key (want one authorized_keys line)", domain.ErrInvalid)
48	}
49	if len(bytes.TrimSpace(rest)) != 0 {
50		return "", "", fmt.Errorf("%w: give exactly one SSH public key", domain.ErrInvalid)
51	}
52	if _, isCert := pk.(*ssh.Certificate); isCert {
53		return "", "", fmt.Errorf("%w: SSH certificates cannot be trusted, give the public key", domain.ErrInvalid)
54	}
55	return ssh.FingerprintSHA256(pk), strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pk))), nil
56}
57
58func inspectGPG(publicKey string) (id, canonical string, err error) {
59	if strings.Count(publicKey, "-----BEGIN PGP") != 1 {
60		return "", "", fmt.Errorf("%w: give exactly one armored OpenPGP public key block", domain.ErrInvalid)
61	}
62	ring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(publicKey))
63	if err != nil {
64		return "", "", fmt.Errorf("%w: not an armored OpenPGP public key", domain.ErrInvalid)
65	}
66	if len(ring) != 1 {
67		return "", "", fmt.Errorf("%w: give exactly one OpenPGP key, got %d", domain.ErrInvalid, len(ring))
68	}
69	e := ring[0]
70	if hasPrivateKey(e) {
71		return "", "", fmt.Errorf("%w: this is a private key; export the public key instead", domain.ErrInvalid)
72	}
73	var out bytes.Buffer
74	w, err := armor.Encode(&out, openpgp.PublicKeyType, nil)
75	if err != nil {
76		return "", "", errors.New("armor the public key")
77	}
78	if err := e.Serialize(w); err != nil {
79		return "", "", fmt.Errorf("%w: unreadable OpenPGP key", domain.ErrInvalid)
80	}
81	if err := w.Close(); err != nil {
82		return "", "", errors.New("armor the public key")
83	}
84	return fingerprintHex(e), out.String(), nil
85}
86
87func hasPrivateKey(e *openpgp.Entity) bool {
88	if e.PrivateKey != nil {
89		return true
90	}
91	for _, s := range e.Subkeys {
92		if s.PrivateKey != nil {
93			return true
94		}
95	}
96	return false
97}
98
99// fingerprintHex is the primary key fingerprint of e in upper-case hex.
100func fingerprintHex(e *openpgp.Entity) string {
101	return strings.ToUpper(hex.EncodeToString(e.PrimaryKey.Fingerprint))
102}

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 4

Tags 45

↑↓ moveenter openesc close