1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package ingress
import (
"context"
"crypto/rand"
"errors"
"strings"
"time"
"github.com/bluesky-social/indigo/atproto/atcrypto"
"github.com/bluesky-social/indigo/atproto/identity"
"github.com/bluesky-social/indigo/atproto/syntax"
"github.com/johnandersen777/socialweb-computer-kcp/internal/didkey"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/opt"
"github.com/johnandersen777/socialweb-computer-kcp/internal/kit/ttlcache"
)
var errNoDirectory = errors.New("ingress: no identity directory to resolve a signing key")
type verifyResult struct {
OK bool
Key string
Reason string
}
type NonceStore struct {
dir identity.Directory
entries *ttlcache.Cache[string, string]
}
func NewNonceStore(ttl time.Duration, dir identity.Directory) *NonceStore {
ttl = opt.Positive(ttl, 60*time.Second)
return &NonceStore{
dir: dir,
entries: ttlcache.New[string, string](ttlcache.Options{TTL: ttl}),
}
}
func (n *NonceStore) Issue(key string) string {
b := make([]byte, 64)
_, _ = rand.Read(b)
nonce := b64encode(b)
n.entries.Set(nonce, key)
return nonce
}
func (n *NonceStore) Verify(ctx context.Context, reg Registration) verifyResult {
if reg.Key == "" {
return verifyResult{Reason: "missing key"}
}
if reg.Nonce == "" {
return verifyResult{Reason: "missing nonce"}
}
key, ok := n.entries.Get(reg.Nonce)
if !ok {
return verifyResult{Reason: "unknown or expired nonce"}
}
if key != reg.Key {
return verifyResult{Reason: "nonce was issued for a different key"}
}
if len(reg.Signatures) == 0 {
return verifyResult{Reason: "no signatures provided"}
}
nonceBytes, err := b64decode(reg.Nonce)
if err != nil {
return verifyResult{Reason: "nonce is not base64"}
}
if n.verifySignatures(ctx, nonceBytes, reg.Signatures) {
n.consume(reg.Nonce)
return verifyResult{OK: true, Key: reg.Key}
}
if n.purgeSignatureKeys(ctx, reg.Signatures) && n.verifySignatures(ctx, nonceBytes, reg.Signatures) {
n.consume(reg.Nonce)
return verifyResult{OK: true, Key: reg.Key}
}
return verifyResult{Reason: "no signature verifies over the nonce"}
}
func (n *NonceStore) consume(nonce string) {
n.entries.Delete(nonce)
}
func (n *NonceStore) verifySignatures(ctx context.Context, nonceBytes []byte, signatures []Signature) bool {
for _, sig := range signatures {
if sig.Key == "" || sig.Signature == "" {
continue
}
pub, err := resolveRegistrationKey(ctx, sig.Key, n.dir)
if err != nil {
continue
}
sigBytes, err := b64decode(sig.Signature)
if err != nil {
continue
}
if err := pub.HashAndVerify(nonceBytes, sigBytes); err == nil {
return true
}
}
return false
}
func (n *NonceStore) purgeSignatureKeys(ctx context.Context, signatures []Signature) bool {
if n.dir == nil {
return false
}
purged := false
for _, sig := range signatures {
if sig.Key == "" || strings.HasPrefix(sig.Key, didkey.Prefix) {
continue
}
did, err := syntax.ParseDID(sig.Key)
if err != nil {
continue
}
if err := n.dir.Purge(ctx, did.AtIdentifier()); err == nil {
purged = true
}
}
return purged
}
func resolveRegistrationKey(ctx context.Context, key string, dir identity.Directory) (atcrypto.PublicKey, error) {
if strings.HasPrefix(key, didkey.Prefix) {
return didkey.ParsePublic(key)
}
if dir == nil {
return nil, errNoDirectory
}
return resolveSigningKey(ctx, key, dir)
}