-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathregistry.go
263 lines (222 loc) · 8.61 KB
/
registry.go
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
// Package registry provides an interface to the Oasis off-chain registry of signed statements.
package registry
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/mail"
"net/url"
"regexp"
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
"github.com/oasisprotocol/oasis-core/go/common/prettyprint"
)
var (
// ErrNoSuchEntity is the error returned where the requested entity cannot be found.
ErrNoSuchEntity = errors.New("registry: no such entity")
// ErrCorruptedRegistry is the error returned where the registry is corrupted (does not conform
// to the specifications or contains data that fails signature verification).
ErrCorruptedRegistry = errors.New("registry: corrupted registry")
)
const (
// MaxStatementSize is the maximum encoded signed statement size in bytes.
MaxStatementSize = 16 * 1024
// MaxEntityNameLength is the maximum length of the entity metadata's Name field.
MaxEntityNameLength = 50
// MaxEntityURLLength is the maximum length of the entity metadata's URL field.
MaxEntityURLLength = 64
// MaxEntityEmailLength is the maximum length of the entity metadata's Email field.
MaxEntityEmailLength = 32
// MaxEntityKeybaseLength is the maximum length of the entity metadata's Keybase field.
MaxEntityKeybaseLength = 32
// MaxEntityTwitterLength is the maximum length of the entity metadata's Twitter field.
MaxEntityTwitterLength = 32
// MinSupportedVersion is the minimum supported entity metadata version.
MinSupportedVersion = 1
// MaxSupportedVersion is the maximum supported entity metadata version.
MaxSupportedVersion = 1
)
var (
// TwitterHandleRegexp is the regular expression used for validating the Twitter field.
TwitterHandleRegexp = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
// KeybaseHandleRegexp is the regular expression used for validating the Keybase field.
KeybaseHandleRegexp = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
)
// Provider is the read-only registry provider interface.
type Provider interface {
// Verify verifies the integrity of the whole registry.
Verify() error
// VerifyUpdate verifies the integrity of a registry update from src.
VerifyUpdate(src Provider) error
// GetEntities returns a list of all entities in the registry.
GetEntities(ctx context.Context) (map[signature.PublicKey]*EntityMetadata, error)
// GetEntity returns metadata for a specific entity.
GetEntity(ctx context.Context, id signature.PublicKey) (*EntityMetadata, error)
}
// EntityMetadataSignatureContext is the domain separation context used for entity metadata.
var EntityMetadataSignatureContext = signature.NewContext("oasis-metadata-registry: entity")
var _ prettyprint.PrettyPrinter = (*EntityMetadata)(nil)
// EntityMetadata contains metadata about an entity.
type EntityMetadata struct {
cbor.Versioned
// Serial is the serial number of the entity metadata statement.
Serial uint64 `json:"serial"`
// Name is the entity name.
Name string `json:"name,omitempty"`
// URL is an URL associated with an entity.
URL string `json:"url,omitempty"`
// Email is the entity's contact e-mail address.
Email string `json:"email,omitempty"`
// Keybase is the keybase.io handle.
Keybase string `json:"keybase,omitempty"`
// Twitter is the Twitter handle.
Twitter string `json:"twitter,omitempty"`
}
// Equal compares vs another entity metadata for equality.
func (e *EntityMetadata) Equal(other *EntityMetadata) bool {
return bytes.Equal(cbor.Marshal(e), cbor.Marshal(other))
}
// validateURL checks validity of the given URL.
func validateURL(u string) error {
if len(u) > MaxEntityURLLength {
return fmt.Errorf("entity URL too long (length: %d max: %d)", len(u), MaxEntityURLLength)
}
if len(u) > 0 {
parsedURL, err := url.Parse(u)
if err != nil {
return fmt.Errorf("entity URL is malformed: %w", err)
}
if parsedURL.Scheme != "https" {
return fmt.Errorf("entity URL must use the https scheme (scheme: %s)", parsedURL.Scheme)
}
if port := parsedURL.Port(); port != "" {
return fmt.Errorf("entity URL must use the default port (port: %s)", port)
}
if len(parsedURL.RawQuery) != 0 || len(parsedURL.Fragment) != 0 {
return fmt.Errorf("entity URL must not contain query values or fragments")
}
}
return nil
}
// ValidateBasic performs basic validity checks on the entity metadata.
func (e *EntityMetadata) ValidateBasic() error {
if e.Versioned.V < MinSupportedVersion || e.Versioned.V > MaxSupportedVersion {
return fmt.Errorf("unsupported entity metadata version: %d", e.Versioned.V)
}
// Name.
if len(e.Name) > MaxEntityNameLength {
return fmt.Errorf("entity name too long (length: %d max: %d)", len(e.Name), MaxEntityNameLength)
}
// URL.
if err := validateURL(e.URL); err != nil {
return err
}
// Email.
if len(e.Email) > MaxEntityEmailLength {
return fmt.Errorf("entity e-mail too long (length: %d max: %d)", len(e.Email), MaxEntityEmailLength)
}
if len(e.Email) > 0 {
parsedEmail, err := mail.ParseAddress(e.Email)
if err != nil {
return fmt.Errorf("entity e-mail is malformed: %w", err)
}
if len(parsedEmail.Name) != 0 {
return fmt.Errorf("entity e-mail must not contain a name")
}
}
// Keybase.
if len(e.Keybase) > MaxEntityKeybaseLength {
return fmt.Errorf("entity keybase handle too long (length: %d max: %d)", len(e.Keybase), MaxEntityKeybaseLength)
}
if len(e.Keybase) > 0 {
if !KeybaseHandleRegexp.MatchString(e.Keybase) {
return fmt.Errorf("entity keybase handle is malformed")
}
}
// Twitter.
if len(e.Twitter) > MaxEntityTwitterLength {
return fmt.Errorf("entity twitter handle too long (length: %d max: %d)", len(e.Twitter), MaxEntityTwitterLength)
}
if len(e.Twitter) > 0 {
if !TwitterHandleRegexp.MatchString(e.Twitter) {
return fmt.Errorf("entity twitter handle is malformed")
}
}
return nil
}
// Load loads and verifies entity metadata from a given reader containing signed entity metadata.
func (e *EntityMetadata) Load(id signature.PublicKey, r io.Reader) error {
b, err := io.ReadAll(r)
if err != nil {
return fmt.Errorf("%w: failed to read metadata: %s", ErrCorruptedRegistry, err)
}
if len(b) > MaxStatementSize {
return fmt.Errorf("%w: statement too big (size: %d max: %d)", ErrCorruptedRegistry, len(b), MaxStatementSize)
}
var sigEntity SignedEntityMetadata
if err = json.Unmarshal(b, &sigEntity); err != nil {
return fmt.Errorf("%w: failed to unmarshal signed entity metadata: %s", ErrCorruptedRegistry, err)
}
if !sigEntity.Signature.PublicKey.Equal(id) {
return fmt.Errorf("%w: entity metadata signer does not match expected entity (expected: %s got: %s)",
ErrCorruptedRegistry,
id,
sigEntity.Signature.PublicKey,
)
}
if err = sigEntity.Open(e); err != nil {
return fmt.Errorf("%w: failed to verify signed entity metadata: %s", ErrCorruptedRegistry, err)
}
if err = e.ValidateBasic(); err != nil {
return fmt.Errorf("%w: failed to validate entity metadata: %s", ErrCorruptedRegistry, err)
}
return nil
}
// PrettyPrint writes a pretty-printed representation of EntityMetadata to the
// given writer.
func (e *EntityMetadata) PrettyPrint(ctx context.Context, prefix string, w io.Writer) {
fmt.Fprintf(w, "%sVersion: %d\n", prefix, e.V)
fmt.Fprintf(w, "%sSerial: %d\n", prefix, e.Serial)
fmt.Fprintf(w, "%sName: %s\n", prefix, e.Name)
fmt.Fprintf(w, "%sURL: %s\n", prefix, e.URL)
fmt.Fprintf(w, "%sEmail: %s\n", prefix, e.Email)
fmt.Fprintf(w, "%sKeybase: %s\n", prefix, e.Keybase)
fmt.Fprintf(w, "%sTwitter: %s\n", prefix, e.Twitter)
}
// PrettyType returns a representation of EntityMetadata that can be used for
// pretty printing.
func (e EntityMetadata) PrettyType() (interface{}, error) {
return e, nil
}
// SignedEntityMetadata is a signed entity metadata statement.
type SignedEntityMetadata struct {
signature.Signed
}
// Open first verifies the blob signature and then unmarshals the blob.
func (s *SignedEntityMetadata) Open(meta *EntityMetadata) error {
return s.Signed.Open(EntityMetadataSignatureContext, meta)
}
// Save serializes and writes entity metadata to the given writer.
func (s *SignedEntityMetadata) Save(w io.Writer) error {
b, err := json.Marshal(s)
if err != nil {
return fmt.Errorf("failed to marshal metadata: %w", err)
}
if _, err = w.Write(b); err != nil {
return fmt.Errorf("failed to write metadata: %w", err)
}
return nil
}
// SignEntityMetadata serializes the EntityMetadata and signs the result.
func SignEntityMetadata(signer signature.Signer, meta *EntityMetadata) (*SignedEntityMetadata, error) {
signed, err := signature.SignSigned(signer, EntityMetadataSignatureContext, meta)
if err != nil {
return nil, err
}
return &SignedEntityMetadata{
Signed: *signed,
}, nil
}