package operator import ( "context" "encoding/base64" "time" family "amadan.net/rastrillo/rastrillo/crypto" ) const InspectContext = "oficina/home/operator-inspect/v1" type InspectChallenge struct { Version int `json:"version"` Purpose string `json:"purpose"` Role string `json:"role"` Origin string `json:"origin"` Installation string `json:"installation"` Registry string `json:"registry"` Device string `json:"device"` Challenge string `json:"challenge"` Issued int64 `json:"issued_at"` Expires int64 `json:"expires_at"` } func (c InspectChallenge) valid() bool { return c.Version == 1 && c.Purpose == "operator-inspect" && (c.Role == "directory" || c.Role == "credential") && Origin(c.Origin) && CanonicalToken(c.Installation) && CanonicalToken(c.Registry) && CanonicalToken(c.Device) && CanonicalToken(c.Challenge) && c.Issued > 0 && c.Expires > c.Issued && c.Expires-c.Issued <= 300 } func SignInspect(k *family.Keypair, c InspectChallenge) ([]byte, error) { if !signingKey(k) || !c.valid() { return nil, ErrRefused } payload, err := encode(c) if err != nil { return nil, ErrRefused } signature, err := family.Sign(k, InspectContext, payload) if err != nil { return nil, ErrUnavailable } return encode(envelope{1, keyID(k.SignPub()), base64.RawURLEncoding.EncodeToString(payload), base64.RawURLEncoding.EncodeToString(signature)}) } func (v *Verifier) verifyInspect(raw []byte, now time.Time) (InspectChallenge, error) { var env envelope var c InspectChallenge if v == nil || len(raw) == 0 || len(raw) > AuthorizationLimit || strictCanonical(raw, &env) != nil || env.Version != 1 || env.KeyID != v.kid { return c, ErrRefused } payload, ok := canonical(env.Payload, -1) sig, ok2 := canonical(env.Signature, 64) if !ok || !ok2 || !family.Verify(v.key, InspectContext, payload, sig) || strictCanonical(payload, &c) != nil || !c.valid() { return InspectChallenge{}, ErrRefused } p := v.pins if c.Role != p.Role || c.Origin != p.Origin || c.Installation != p.Installation || c.Registry != p.Registry || c.Device != p.Device || c.Issued > now.Unix() || c.Expires <= now.Unix() { return InspectChallenge{}, ErrRefused } return c, nil } type Inspection struct { Version int `json:"version"` Challenge string `json:"challenge"` Observed int64 `json:"observed_at"` Generation *string `json:"generation"` TeamID int64 `json:"proposed_team_id"` TeamSQID string `json:"proposed_team_sqid"` } // Inspect observes the next proposal without reserving it or advancing authority. // Its private result still requires the platform's attempt-bound result channel. func (d *Directory) Inspect(ctx context.Context, raw []byte) ([]byte, error) { if d == nil || d.engine == nil { return nil, ErrRefused } e := d.engine challenge, err := e.verifier.verifyInspect(raw, e.now()) if err != nil { return nil, err } ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() c, err := e.db.Conn(ctx) if err != nil { return nil, ErrUnavailable } defer c.Close() if _, err = c.ExecContext(ctx, "BEGIN"); err != nil { return nil, ErrUnavailable } defer c.ExecContext(context.Background(), "ROLLBACK") if e.checkSchema != nil { if _, err := c.ExecContext(ctx, "PRAGMA query_only=ON"); err != nil { return nil, ErrUnavailable } var readonly int if c.QueryRowContext(ctx, "PRAGMA query_only").Scan(&readonly) != nil || readonly != 1 { return nil, ErrUnavailable } if err := e.checkSchema(ctx, c); err != nil { return nil, err } } var count int if err = c.QueryRowContext(ctx, "SELECT count(*) FROM directory_operator_state WHERE singleton=1 AND (origin!=? OR installation!=? OR registry!=? OR device!=? OR key_id!=? OR completed_at>?)", e.verifier.pins.Origin, e.verifier.pins.Installation, e.verifier.pins.Registry, e.verifier.pins.Device, e.verifier.kid, e.now().Unix()).Scan(&count); err != nil { return nil, ErrUnavailable } if count != 0 { return nil, ErrRefused } var generation *string rows, err := c.QueryContext(ctx, "SELECT generation FROM roster_generation WHERE singleton=1") if err != nil { return nil, ErrUnavailable } if rows.Next() { var g string if rows.Scan(&g) != nil || !CanonicalToken(g) { rows.Close() return nil, ErrUnavailable } generation = &g } err = rows.Err() closeErr := rows.Close() if err != nil || closeErr != nil { return nil, ErrUnavailable } id, sqid, err := nextTeam(ctx, c) if err != nil { return nil, ErrUnavailable } if _, err = e.verifier.verifyInspect(raw, e.now()); err != nil { return nil, ErrRefused } out, err := encode(Inspection{1, challenge.Challenge, e.now().Unix(), generation, id, sqid}) if err != nil { return nil, ErrUnavailable } if _, err = c.ExecContext(ctx, "COMMIT"); err != nil { return nil, ErrUnavailable } return out, nil }