1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore
4
5import (
6 "context"
7 "fmt"
8 "slices"
9 "strings"
10
11 kv "bnema.dev/ermite-kv"
12 "google.golang.org/protobuf/proto"
13
14 storagepb "bnema.dev/ermite/gen/ermite/storage"
15 "bnema.dev/ermite/internal/domain"
16 "bnema.dev/ermite/internal/ports"
17)
18
19// Key layout of the signing keys of the OIDC ID tokens (CI-4):
20//
21// oidc/key/<kid> OIDCKey (the private key sealed, see secretbox)
22const oidcKeyPrefix = "oidc/key/"
23
24var _ ports.OIDCKeyStore = (*Store)(nil)
25
26// ListOIDCKeys returns every stored key, oldest first.
27func (s *Store) ListOIDCKeys(ctx context.Context) ([]domain.OIDCKey, error) {
28 var out []domain.OIDCKey
29 for key, item := range s.db.Snapshot(ctx).Prefix(ctx, []byte(oidcKeyPrefix)) {
30 var pb storagepb.OIDCKey
31 if err := proto.Unmarshal(item.Value, &pb); err != nil {
32 return nil, fmt.Errorf("decode oidc key %s: %w", key, err)
33 }
34 out = append(out, domain.OIDCKey{
35 ID: strings.TrimPrefix(string(key), oidcKeyPrefix), Sealed: pb.GetSealed(),
36 CreatedAt: decodeTime(pb.GetCreatedAt()), RetiredAt: decodeTime(pb.GetRetiredAt()),
37 })
38 }
39 slices.SortStableFunc(out, func(a, b domain.OIDCKey) int { return a.CreatedAt.Compare(b.CreatedAt) })
40 return out, nil
41}
42
43// SaveOIDCKeys writes the keys of put (created or replaced) and deletes those
44// of drop, in one batch.
45func (s *Store) SaveOIDCKeys(ctx context.Context, put []domain.OIDCKey, drop []string) error {
46 var b kv.Batch
47 for _, k := range put {
48 if k.ID == "" || strings.Contains(k.ID, "/") {
49 return fmt.Errorf("%w: oidc key id %q", domain.ErrInvalid, k.ID)
50 }
51 b.Put([]byte(domain.OIDCKeyPath(k.ID)), mustMarshal(&storagepb.OIDCKey{
52 Sealed: k.Sealed, CreatedAt: encodeTime(k.CreatedAt), RetiredAt: encodeTime(k.RetiredAt),
53 }))
54 }
55 for _, id := range drop {
56 b.Delete([]byte(domain.OIDCKeyPath(id)))
57 }
58 if _, err := s.db.Apply(ctx, &b); err != nil {
59 return fmt.Errorf("store the oidc keys: %w", err)
60 }
61 return nil
62}
63
64// RawOIDCKey returns the stored value of the key, as is. Tests use it to check
65// that no private key is stored in the clear.
66func (s *Store) RawOIDCKey(ctx context.Context, kid string) ([]byte, bool) {
67 item, ok := s.db.Get(ctx, []byte(domain.OIDCKeyPath(kid)))
68 if !ok {
69 return nil, false
70 }
71 return item.Value, true
72}