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}