Repository navigation
Expand file tree
/
Copy pathwork_context_boundary_test.go
More file actions
407 lines (393 loc) · 17.3 KB
/
Copy pathwork_context_boundary_test.go
File metadata and controls
407 lines (393 loc) · 17.3 KB
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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
package solution
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strings"
"testing"
)
// There is one implementation of a Work Context — core's — and this runtime is
// not it. This package obtains a capability through the SDK's mint client,
// carries it, and hands it to whatever verifies it; it never signs one, never
// parses one, and never declares the fields of one.
//
// The gate exists because the duplication it prevents already happened once, in
// a module whose only job was to carry these things: a second signed encoding
// grew inside an SDK beside core's, with its own payload struct, its own
// signer, its own verifier and its own error taxonomy, and 3,532 lines had to
// be deleted to get back to one. Every step of that was locally reasonable. The
// cheapest way to not repeat it is to make the first step fail a test here.
//
// What this checks is narrow on purpose: a signing primitive, and a type or
// function of this package's own that claims to be a Work Context. It does not
// forbid encoding/json — this package serves JSON documents for a living — and
// the one capability-shaped JSON this package does touch, the mint endpoint's
// HTTP bodies, lives in the SDK client and not here.
func TestNoWorkContextImplementationGrowsHere(t *testing.T) {
fset := token.NewFileSet()
// Every source first: a capability type can be aliased in one file and
// decoded into in another.
sources := map[string]*ast.File{}
for _, name := range moduleSources(t) {
file, err := parser.ParseFile(fset, name, nil, parser.ParseComments)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
sources[name] = file
}
for name, file := range sources {
for _, imported := range file.Imports {
path := strings.Trim(imported.Path.Value, `"`)
switch path {
case "crypto/ed25519", "crypto/ecdsa", "crypto/hmac", "crypto/rsa", "crypto/ed25519/internal/edwards25519":
t.Errorf("%s imports %s: signing a capability is core's, and a runtime that can sign one is a second implementation waiting to happen. Carry the credential the mint client hands you.",
name, path)
}
// Core's generated protobuf package is the other road to a
// capability's fields: holding basev0 means declaring, building
// and decoding core's wire types directly, which is a second
// reader of the encoding whatever it is named. Nothing here needs
// it — the SDK re-exports the one type this runtime reads as
// workcontext.SealedValues, and reads it through accessors.
if strings.Contains(path, "codefly-dev/core/generated/") {
t.Errorf("%s imports %s: core's generated wire types are how a second reader of the encoding starts. The credential's seal is reached through the SDK's Credential.Seal() accessor, which needs no generated import.",
name, path)
}
// A JOSE or raw-segment path reaches the same place by another
// road: the 3,532 lines that had to be deleted were a payload
// struct, a signer, a verifier and an error taxonomy, and none of
// them needed crypto/ed25519 by name.
switch {
case strings.HasPrefix(path, "golang.org/x/crypto"),
strings.Contains(path, "go-jose"), strings.Contains(path, "jwt"),
strings.Contains(path, "jws"), strings.Contains(path, "jwk"):
t.Errorf("%s imports %s: a second signed encoding beside core's is what this boundary exists to prevent, and it does not have to be spelled crypto/ed25519 to be one.",
name, path)
}
}
// Constructing and decoding one of core's capability messages used to
// be checked here, syntactically. Both are in
// typed_boundary_test.go now
// (TestNoCapabilityIsConstructedOrDecodedByType), keyed on core's
// GENERATED PACKAGE PATH rather than on a set of message names — so an
// alias, a function-local alias, an assignment, a supplied destination
// and a message core adds tomorrow are all one case. The list of names
// was the defect: it knew WorkContextV1 and WorkSealV1, the SDK
// exports those as Claims and SealedValues, and three rounds went into
// adding names to it.
// A declaration of this package's own that names itself after the thing
// core owns. The two this package legitimately has are a refusal it
// returns to a handler and the principals it reports from a mint
// answer — neither is a capability, and both are named below so a third
// has to be argued for rather than appearing.
allowed := map[string]bool{"WorkContextRefusal": true, "WorkContextPrincipals": true}
for _, decl := range file.Decls {
var declared string
switch node := decl.(type) {
case *ast.FuncDecl:
declared = node.Name.Name
case *ast.GenDecl:
for _, spec := range node.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok && strings.Contains(typeSpec.Name.Name, "WorkContext") && !allowed[typeSpec.Name.Name] {
t.Errorf("%s declares the type %s: a Work Context type in this package is core's type copied. Use the SDK's alias of core's, or carry the capability as the string it travels as.",
name, typeSpec.Name.Name)
}
}
continue
}
if strings.Contains(declared, "SignWorkContext") || strings.Contains(declared, "ParseWorkContext") || strings.Contains(declared, "VerifyWorkContext") {
t.Errorf("%s declares %s: signing, parsing and verifying a capability are core's. This runtime presents one and lets the far end decide.", name, declared)
}
}
}
}
// And the other half of not being an implementation: this runtime does not
// verify, and that is a design position rather than an omission.
//
// Core's verifier requires four live sources — the authorization revision,
// replay, grants and seals — and refuses everything without them, deliberately,
// so that the strongest check in the model cannot be the easiest to skip. A
// solution runtime holds none of those: it is the party presenting a
// capability, not the party that decides on one. So there is no verifier here
// to configure, and a future change that adds one has to answer where those
// four sources come from.
//
// This is not an exemption from Core's conformance kit. The kit runs — against
// the boundary this package does own, carriage, in
// TestCoreConformanceFixturesThroughTheCarrier — and it is what keeps the line
// below checkable: every fixture whose refusal is a property of the bytes in
// hand is refused here with Core's own sentinel, and every fixture whose
// refusal is a judgement against live state is carried, because claiming that
// judgement without the issuer's four sources is the silent downgrade this
// single-implementation rule exists to prevent.
func TestThisRuntimeVerifiesNothing(t *testing.T) {
fset := token.NewFileSet()
for _, name := range moduleSources(t) {
file, err := parser.ParseFile(fset, name, nil, 0)
if err != nil {
t.Fatalf("parse %s: %v", name, err)
}
for _, reached := range verifiersReachedBy(file) {
t.Errorf("%s names %s: verifying a capability needs the issuer's live revision, replay, grant and seal sources, which this runtime does not hold. It presents its own credential and lets the component that holds them decide.",
name, reached)
}
if dotted := dotImportedWorkContext(file); dotted != "" {
// A dot-import makes every name in that package unqualified, so
// nothing above could find them. It is also not a style this
// package uses anywhere.
t.Errorf("%s dot-imports %s: the names a verifier is built from would then be unqualified, which this gate cannot see and a reader cannot either", name, dotted)
}
}
}
// verifiersReachedBy is every way one file reaches a Work Context verifier,
// rendered as alias.Name.
//
// Separated from the test so the gate can be run against a file the test
// writes, which is the only way to show it catches an escape rather than
// merely passing over this package's current contents.
func verifiersReachedBy(file *ast.File) []string {
var reached []string
for alias := range workContextImports(file) {
if alias == "." {
continue
}
ast.Inspect(file, func(node ast.Node) bool {
selector, ok := node.(*ast.SelectorExpr)
if !ok {
return true
}
pkg, ok := selector.X.(*ast.Ident)
if !ok || pkg.Name != alias {
return true
}
// Every name a verifier can be reached by: the constructor, the
// type (so a declared field or variable is caught as well as a
// call), and the methods. A verifier cannot be obtained without
// naming one of them through the package it lives in, which is
// what makes matching on the name sufficient here — the earlier
// rule matched only the call, so declaring the type and calling
// a method on the value escaped it.
//
// Contains, not HasPrefix: the probe below caught that a prefix
// rule misses NewVerifier, which is the most ordinary name a
// verifier constructor can have. Nothing this package legitimately
// names through these imports contains it.
//
// And not "Verif" alone. Core v0.19 has two more entrypoints that
// check a signature against an issuer's keys, and neither is
// spelled that way: Inspector, a forwarding hop's check, and
// Authenticator, a callee's check against caller-supplied state.
// Inspector is the sharper of the two, because this runtime used
// to call core's STRUCTURAL read Inspect — renamed Decode in core
// v0.15 precisely so a structural read and a signature check would
// stop sharing a name — and the old spelling is now the one that
// verifies.
for _, verifies := range []string{"Verif", "Inspect", "Authenticat"} {
if strings.Contains(selector.Sel.Name, verifies) {
reached = append(reached, pkg.Name+"."+selector.Sel.Name)
break
}
}
return true
})
}
return reached
}
// dotImportedWorkContext is the Work Context package this file dot-imports, if
// it does.
func dotImportedWorkContext(file *ast.File) string {
for alias, path := range workContextImports(file) {
if alias == "." {
return path
}
}
return ""
}
// TestTheVerifierGateCatchesASubpackage runs the gate against the escapes it
// has actually been shown to miss.
//
// The rule matched two import paths exactly, and
// core/workcontext/conformance — the published conformance kit this suite
// already uses — is neither of them while exporting Verifier(), which returns
// core's own *workcontext.Verifier. So the single import that hands this
// package a working verifier walked past the gate written to refuse exactly
// that. A gate on an enumerated path is only as good as the enumeration.
func TestTheVerifierGateCatchesASubpackage(t *testing.T) {
for _, tc := range []struct {
name string
source string
}{
{
"the conformance subpackage under its own name",
`package solution
import "github.com/codefly-dev/core/workcontext/conformance"
func f() { _ = conformance.Verifier() }`,
},
{
"the conformance subpackage under an alias",
`package solution
import kit "github.com/codefly-dev/core/workcontext/conformance"
func f() { _ = kit.Verifier() }`,
},
{
"a deeper subpackage nobody has written yet",
`package solution
import "github.com/codefly-dev/sdk-go/workcontext/whatever/next"
func f() { _ = next.NewVerifier() }`,
},
{
"the package itself, which already failed the gate",
`package solution
import "github.com/codefly-dev/core/workcontext"
func f() { _ = workcontext.Verifier() }`,
},
{
"a forwarding hop's signature check, which names no Verif",
`package solution
import corework "github.com/codefly-dev/core/workcontext"
func f(token string) { _, _ = (&corework.Inspector{}).Inspect(token, "route") }`,
},
{
"a callee's check against supplied state, which names no Verif either",
`package solution
import corework "github.com/codefly-dev/core/workcontext"
var a corework.Authenticator`,
},
} {
t.Run(tc.name, func(t *testing.T) {
file, err := parser.ParseFile(token.NewFileSet(), "escape.go", tc.source, 0)
if err != nil {
t.Fatalf("parse the probe: %v", err)
}
if reached := verifiersReachedBy(file); len(reached) == 0 {
t.Error("the gate did not see a verifier this file reaches, so a file shaped like this could verify a capability here with nothing noticing")
}
})
}
// And the control: importing the kit without reaching a verifier is not
// flagged. conformance_test.go does exactly this — it reads core's
// published fixtures — so a gate that refused the import itself would
// refuse the thing this suite is built on.
file, err := parser.ParseFile(token.NewFileSet(), "fine.go", `package solution
import "github.com/codefly-dev/core/workcontext/conformance"
func f() { _ = conformance.Fixtures() }`, 0)
if err != nil {
t.Fatalf("parse the control: %v", err)
}
if reached := verifiersReachedBy(file); len(reached) != 0 {
t.Errorf("the gate flagged %v in a file that reaches no verifier: reading core's published fixtures is how this suite gets a capability it did not sign itself", reached)
}
// And core's structural read, which is how this runtime reads the
// credential it was issued. It checks no signature, so it is not a
// verifier, and refusing it would push this package back to a hand-written
// parser of its own.
file, err = parser.ParseFile(token.NewFileSet(), "decode.go", `package solution
import corework "github.com/codefly-dev/core/workcontext"
func f(token string) { decoded, _ := corework.Decode(token); var _ *corework.Decoded = decoded }`, 0)
if err != nil {
t.Fatalf("parse the control: %v", err)
}
if reached := verifiersReachedBy(file); len(reached) != 0 {
t.Errorf("the gate flagged %v in a file that only decodes: core's Decode checks no signature, and it is the one structural reader this runtime is allowed", reached)
}
}
// TestATestingTBParameterIsNotAGate records why the seam's refusal is
// testing.Testing() and not its signature.
//
// testing.TB has an unexported method, which stops a type *declaring* the
// interface — and not a type that EMBEDS it. So a production program can
// satisfy testing.TB in a few lines, which is what the review's reproducer
// did, and a function taking one proves nothing about where it was called
// from.
func TestATestingTBParameterIsNotAGate(t *testing.T) {
// The point is that this line compiles: a struct embedding testing.TB is a
// testing.TB anywhere, including in a deployment, so a function taking one
// proves nothing about where it was called from.
var satisfied testing.TB = struct{ testing.TB }{}
_ = satisfied
}
// workContextImports is the local name each Work Context package is imported
// under in one file, which is what the gate above has to match on rather than
// the name this package happens to use.
func workContextImports(file *ast.File) map[string]string {
imports := map[string]string{}
for _, imported := range file.Imports {
path := strings.Trim(imported.Path.Value, `"`)
// Prefix, not equality, so a *subpackage* is matched too. The rule was
// written against the two package paths exactly, and
// core/workcontext/conformance is neither of them while exporting
// Verifier(), which returns core's own *workcontext.Verifier — so the
// one import that hands this package a working verifier passed the
// gate meant to refuse exactly that. A gate on a path is only as good
// as the paths it enumerates, which is the same defect as an
// enumerated posture check.
if !isWorkContextPath(path) {
continue
}
alias := path[strings.LastIndex(path, "/")+1:]
if imported.Name != nil {
alias = imported.Name.Name
}
if alias == "_" {
continue
}
imports[alias] = path
}
return imports
}
// isWorkContextPath reports whether an import path is a Work Context package or
// anything beneath one, in either repository that owns one.
func isWorkContextPath(path string) bool {
for _, root := range []string{
"github.com/codefly-dev/core/workcontext",
"github.com/codefly-dev/sdk-go/workcontext",
} {
if path == root || strings.HasPrefix(path, root+"/") {
return true
}
}
return false
}
// The handler-export, capability-decoder and seal-construction rules used to
// live here, syntactically: name lists, rendered type spellings, alias maps and
// a probe list that every review round extended by one shape. They are in
// typed_boundary_test.go now, built on go/types, where an alias, an anonymous
// struct, a supplied destination and an assignment are all resolved rather than
// matched. What stays in this file is what is genuinely syntactic: the import
// rules, the declaration-name rule and the verifier rule.
// moduleSources is every non-test Go source in this module, which is what both
// gates above read.
//
// Every directory, not only the root: a gate that does not look at the
// directory most likely to break it is a gate that reports success for the
// wrong reason.
func moduleSources(t *testing.T) []string {
t.Helper()
var sources []string
err := filepath.WalkDir(".", func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.IsDir() {
// Nothing generated and nothing vendored: those are not this
// repository's declarations to answer for.
if name := entry.Name(); path != "." && (name == "vendor" || strings.HasPrefix(name, ".")) {
return fs.SkipDir
}
return nil
}
if filepath.Ext(path) == ".go" && !strings.HasSuffix(path, "_test.go") {
sources = append(sources, path)
}
return nil
})
if err != nil {
t.Fatalf("walk module sources: %v", err)
}
if len(sources) < 2 {
t.Fatalf("found %d sources to gate, which cannot be right: a gate that reads nothing passes", len(sources))
}
return sources
}