1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "slices"
10 "strings"
11 "unicode/utf8"
12
13 kv "bnema.dev/ermite-kv"
14 "google.golang.org/protobuf/proto"
15
16 storagepb "bnema.dev/ermite/gen/ermite/storage"
17 "bnema.dev/ermite/internal/domain"
18 "bnema.dev/ermite/internal/ports"
19)
20
21const (
22 trustedKeyPrefix = "signing/trusted/"
23
24 // maxTrustedKeyIDLen bounds a key id: a fingerprint is 50 bytes at most.
25 maxTrustedKeyIDLen = 128
26)
27
28// TrustedKeys is the ermite-kv backed ports.TrustedKeyStore. It shares the
29// database of the Store it comes from.
30type TrustedKeys struct {
31 db *kv.DB
32}
33
34var _ ports.TrustedKeyStore = (*TrustedKeys)(nil)
35
36// TrustedKeys returns the trusted key store over the same database. It is
37// valid until the Store is closed.
38func (s *Store) TrustedKeys() *TrustedKeys { return &TrustedKeys{db: s.db} }
39
40func trustedKeyKey(id string) []byte { return []byte(trustedKeyPrefix + id) }
41
42// ListTrustedKeys returns the stored keys, oldest first, then by id.
43func (s *TrustedKeys) ListTrustedKeys(ctx context.Context) ([]domain.TrustedKey, error) {
44 var out []domain.TrustedKey
45 for key, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte(trustedKeyPrefix)) {
46 k, err := decodeTrustedKey(item.Value)
47 if err != nil {
48 return nil, fmt.Errorf("decode %s: %w", key, err)
49 }
50 out = append(out, k)
51 }
52 slices.SortStableFunc(out, func(a, b domain.TrustedKey) int {
53 if c := a.CreatedAt.Compare(b.CreatedAt); c != 0 {
54 return c
55 }
56 return strings.Compare(a.ID, b.ID)
57 })
58 return out, nil
59}
60
61// CreateTrustedKey stores signing/trusted/<id> with an "absent" precondition:
62// a taken id yields domain.ErrAlreadyExists. The caller bounds the number of
63// keys.
64func (s *TrustedKeys) CreateTrustedKey(ctx context.Context, k domain.TrustedKey) error {
65 val, err := encodeTrustedKey(k)
66 if err != nil {
67 return err
68 }
69 var b kv.Batch
70 b.RequireAbsent(trustedKeyKey(k.ID))
71 b.Put(trustedKeyKey(k.ID), val)
72 if _, err := s.db.Apply(ctx, &b); err != nil {
73 if errors.Is(err, kv.ErrConflict) {
74 return domain.ErrAlreadyExists
75 }
76 return fmt.Errorf("create trusted key: %w", err)
77 }
78 return nil
79}
80
81// DeleteTrustedKey removes signing/trusted/<id>, or returns domain.ErrNotFound.
82func (s *TrustedKeys) DeleteTrustedKey(ctx context.Context, id string) error {
83 if !validTrustedKeyID(id) {
84 return domain.ErrNotFound
85 }
86 for range updateAttempts {
87 item, ok := s.db.Get(ctx, trustedKeyKey(id))
88 if !ok {
89 return domain.ErrNotFound
90 }
91 var b kv.Batch
92 b.RequireVersion(trustedKeyKey(id), item.Version)
93 b.Delete(trustedKeyKey(id))
94 _, err := s.db.Apply(ctx, &b)
95 if errors.Is(err, kv.ErrConflict) {
96 continue // changed or removed meanwhile: read again
97 }
98 if err != nil {
99 return fmt.Errorf("delete trusted key: %w", err)
100 }
101 return nil
102 }
103 return fmt.Errorf("delete trusted key: %w", domain.ErrConflict)
104}
105
106// validTrustedKeyID reports whether id can be a key id: a fingerprint, so
107// non-empty, bounded, valid UTF-8 text.
108func validTrustedKeyID(id string) bool {
109 return id != "" && len(id) <= maxTrustedKeyIDLen && utf8.ValidString(id)
110}
111
112func encodeTrustedKey(k domain.TrustedKey) ([]byte, error) {
113 if !validTrustedKeyID(k.ID) {
114 return nil, fmt.Errorf("%w: trusted key id has %d bytes", domain.ErrInvalid, len(k.ID))
115 }
116 texts := append([]string{string(k.Format), k.Title, k.PublicKey}, k.Emails...)
117 for _, s := range texts {
118 if !utf8.ValidString(s) {
119 return nil, fmt.Errorf("%w: trusted key text is not valid UTF-8", domain.ErrInvalid)
120 }
121 }
122 return mustMarshal(&storagepb.TrustedKey{
123 Id: k.ID,
124 Format: string(k.Format),
125 Title: k.Title,
126 PublicKey: k.PublicKey,
127 Emails: k.Emails,
128 CreatedAt: encodeTime(k.CreatedAt),
129 }), nil
130}
131
132func decodeTrustedKey(val []byte) (domain.TrustedKey, error) {
133 var pb storagepb.TrustedKey
134 if err := proto.Unmarshal(val, &pb); err != nil {
135 return domain.TrustedKey{}, fmt.Errorf("decode trusted key: %w", err)
136 }
137 if !validTrustedKeyID(pb.GetId()) {
138 return domain.TrustedKey{}, fmt.Errorf("decode trusted key: id has %d bytes", len(pb.GetId()))
139 }
140 return domain.TrustedKey{
141 ID: pb.GetId(),
142 Format: domain.SignatureFormat(pb.GetFormat()),
143 Title: pb.GetTitle(),
144 PublicKey: pb.GetPublicKey(),
145 Emails: slices.Clone(pb.GetEmails()),
146 CreatedAt: decodeTime(pb.GetCreatedAt()),
147 }, nil
148}