package operator import ( "bytes" "encoding/base64" "os" "strings" "testing" "time" family "amadan.net/rastrillo/rastrillo/crypto" ) func schemaWireFixture(t *testing.T) (*family.Keypair, *Verifier, *schemaVerifierV2, schemaAuthorizationV2) { t.Helper() key, old, a := fixture(t) a.Version = 2 p := old.pins p.Actions = append(p.Actions, "directory.inspect") v, err := newSchemaVerifierV2(p, "directory-historical-empty-v1", strings.Repeat("a", 64)) if err != nil { t.Fatal(err) } return key, old, v, schemaAuthorizationV2{a, v.profile, v.manifestDigest} } func TestSchemaWireVersionsAndSignedPins(t *testing.T) { key, old, v, a := schemaWireFixture(t) raw, err := signSchemaRequestV2(key, a, struct { Generation string `json:"generation"` }{token(8)}) if err != nil { t.Fatal(err) } r, err := verifySchemaRequestV2(v, raw) if err != nil || r.authorization.Version != 2 || r.head == "" { t.Fatal(err) } if _, err := old.Verify(raw); err == nil { t.Fatal("public v1 verifier accepted v2") } oldAuth := a.Authorization oldAuth.Version = 1 if _, err := verifySchemaRequestV2(v, signed(t, key, oldAuth)); err == nil { t.Fatal("v2 accepted v1") } if _, err := SignRequest(key, a.Authorization, map[string]string{}); err == nil { t.Fatal("public signer accepted v2") } for _, change := range []func(*schemaAuthorizationV2){ func(a *schemaAuthorizationV2) { a.SchemaProfile = "directory-current-v1" }, func(a *schemaAuthorizationV2) { a.SchemaManifestDigest = strings.Repeat("b", 64) }, func(a *schemaAuthorizationV2) { a.Origin = "https://other.example.test" }, func(a *schemaAuthorizationV2) { a.Installation = token(19) }, func(a *schemaAuthorizationV2) { a.Registry = token(19) }, func(a *schemaAuthorizationV2) { a.Device = token(19) }, func(a *schemaAuthorizationV2) { a.Action = "directory.activate-consumer" }, } { changed := a change(&changed) wire, err := signSchemaRequestV2(key, changed, map[string]string{}) if err != nil { t.Fatal(err) } if _, err := verifySchemaRequestV2(v, wire); err == nil { t.Fatal("trusted key authorized another target/profile") } } for _, bad := range []schemaAuthorizationV2{ {a.Authorization, "", a.SchemaManifestDigest}, {a.Authorization, "unknown", a.SchemaManifestDigest}, {a.Authorization, a.SchemaProfile, ""}, {a.Authorization, a.SchemaProfile, strings.Repeat("A", 64)}, } { if _, err := signSchemaRequestV2(key, bad, map[string]string{}); err == nil { t.Fatal("invalid commitment signed") } } var w wrapper if strictCanonical(raw, &w) != nil { t.Fatal("wrapper") } for _, version := range []int{0, 1, 3} { changed := w changed.Version = version wire, _ := encode(changed) if _, err := verifySchemaRequestV2(v, wire); err == nil { t.Fatal("mixed wrapper accepted") } } var env envelope if strictCanonical(w.Authorization, &env) != nil { t.Fatal("envelope") } env.Version = 2 changed := w changed.Authorization, _ = encode(env) wire, _ := encode(changed) if _, err := verifySchemaRequestV2(v, wire); err == nil { t.Fatal("envelope v2 accepted") } changed = w changed.Input = []byte(`{"generation":"changed"}`) wire, _ = encode(changed) if _, err := verifySchemaRequestV2(v, wire); err == nil { t.Fatal("changed input accepted") } if _, err := verifySchemaRequestV2(v, append([]byte{' '}, raw...)); err == nil { t.Fatal("noncanonical wire") } if _, err := verifySchemaRequestV2(v, bytes.Replace(raw, []byte(`"version":2`), []byte(`"version":2,"version":2`), 1)); err == nil { t.Fatal("duplicate member") } } func TestSchemaInspectUsesOwnContextAndFreshPins(t *testing.T) { key, old, v, a := schemaWireFixture(t) c := schemaInspectV2{InspectChallenge{2, "operator-inspect", a.Role, a.Origin, a.Installation, a.Registry, a.Device, token(6), a.Issued, a.Expires}, a.SchemaProfile, a.SchemaManifestDigest} raw, err := signSchemaInspectV2(key, c) if err != nil { t.Fatal(err) } now := time.Unix(a.Issued, 0) if _, err := verifySchemaInspectV2(v, raw, now); err != nil { t.Fatal(err) } if _, err := old.verifyInspect(raw, now); err == nil { t.Fatal("v1 inspector accepted v2") } if _, err := SignInspect(key, c.InspectChallenge); err == nil { t.Fatal("public inspection signer accepted v2") } for _, at := range []int64{a.Issued - 1, a.Expires, a.Expires + 1} { if _, err := verifySchemaInspectV2(v, raw, time.Unix(at, 0)); err == nil { t.Fatal("stale inspection accepted") } } changed := c changed.SchemaProfile = "directory-current-v1" wire, _ := signSchemaInspectV2(key, changed) if _, err := verifySchemaInspectV2(v, wire, now); err == nil { t.Fatal("other signed profile accepted") } var env envelope if strictCanonical(raw, &env) != nil { t.Fatal("envelope") } payload, _ := base64.RawURLEncoding.DecodeString(env.Payload) sig, err := family.Sign(key, InputContext, payload) if err != nil { t.Fatal(err) } env.Signature = base64.RawURLEncoding.EncodeToString(sig) wire, _ = encode(env) if _, err := verifySchemaInspectV2(v, wire, now); err == nil { t.Fatal("inspection signed for mutation context accepted") } oldChallenge := c.InspectChallenge oldChallenge.Version = 1 wire, err = SignInspect(key, oldChallenge) if err != nil { t.Fatal(err) } if _, err := verifySchemaInspectV2(v, wire, now); err == nil { t.Fatal("v2 inspector accepted v1") } } func TestSchemaWireKeepsV1PayloadBytes(t *testing.T) { _, _, a := fixture(t) a.InputDigest = token(7) c := InspectChallenge{1, "operator-inspect", a.Role, a.Origin, a.Installation, a.Registry, a.Device, token(6), a.Issued, a.Expires} for name, value := range map[string]any{"authorization": a, "inspection": c} { raw, err := encode(value) if err != nil { t.Fatal(err) } path := "testdata/schema-v1-" + name + ".json" if os.Getenv("OFICINA_CAPTURE_V1_SCHEMA_WIRE") == "1" { if err := os.WriteFile(path, raw, 0600); err != nil { t.Fatal(err) } continue } frozen, err := os.ReadFile(path) if err != nil || !bytes.Equal(frozen, raw) { t.Fatal("v1 signature input changed", name, err) } } }