1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package kvstore
4
5import (
6 "context"
7 "errors"
8 "fmt"
9 "time"
10
11 kv "bnema.dev/ermite-kv"
12 "google.golang.org/protobuf/proto"
13
14 storagepb "bnema.dev/ermite/gen/ermite/storage"
15)
16
17const signingKeyKey = "signing/key"
18
19// SigningSeedLen is the length of the stored seed of the signing key.
20const SigningSeedLen = 32
21
22// SigningSeed returns the 32-byte seed of the commit signing key (ADR 0022).
23// When none is stored, it calls generate, stores what it returns and returns
24// that; of two concurrent first calls one wins and both return its seed. The
25// seed is the private key: neither it nor an error carrying it is logged.
26func (s *Store) SigningSeed(ctx context.Context, generate func() ([]byte, error), now time.Time) ([]byte, error) {
27 key := []byte(signingKeyKey)
28 for range 2 {
29 if item, ok := s.db.Get(ctx, key); ok {
30 return decodeSigningSeed(item.Value)
31 }
32 seed, err := generate()
33 if err != nil {
34 return nil, fmt.Errorf("generate the signing key: %w", err)
35 }
36 if len(seed) != SigningSeedLen {
37 return nil, fmt.Errorf("generate the signing key: seed has %d bytes, want %d", len(seed), SigningSeedLen)
38 }
39 var b kv.Batch
40 b.RequireAbsent(key)
41 b.Put(key, mustMarshal(&storagepb.SigningKey{Seed: seed, CreatedAt: encodeTime(now)}))
42 _, err = s.db.Apply(ctx, &b)
43 if errors.Is(err, kv.ErrConflict) {
44 continue // written meanwhile: read it
45 }
46 if err != nil {
47 return nil, fmt.Errorf("store the signing key: %w", err)
48 }
49 return seed, nil
50 }
51 return nil, errors.New("store the signing key: concurrent initialization did not settle")
52}
53
54func decodeSigningSeed(val []byte) ([]byte, error) {
55 var pb storagepb.SigningKey
56 if err := proto.Unmarshal(val, &pb); err != nil {
57 return nil, fmt.Errorf("decode the signing key: %w", err)
58 }
59 if len(pb.GetSeed()) != SigningSeedLen {
60 return nil, fmt.Errorf("decode the signing key: seed has %d bytes, want %d", len(pb.GetSeed()), SigningSeedLen)
61 }
62 return pb.GetSeed(), nil
63}