carlos / platform Public

reqrec, console: 8-byte nonce (40-char codes), one canonical code for verify and lookup, record_status found/absent/unavailable (final review I5, I7, I8)

The exemption comment for the edge-refs route now describes what it
does: an authenticated, app-scoped read; unsealed or unverifiable codes
answer verified:false; forged codes get the gate's 404.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 6b477073bbca9d215c88a2c9f515a42ca50d48c0 parent ed68158
5 files changed, +212 −58
  • internal/console/apiedgeref.go +38 −36
  • internal/console/apiedgeref_test.go +53 −4
  • internal/console/svcroutes_test.go +5 −3
  • internal/reqrec/ref.go +35 −13
  • internal/reqrec/ref_test.go +81 −2
diff --git a/internal/console/apiedgeref.go b/internal/console/apiedgeref.go
index 1b4293d92..afc8896b7 100644
--- a/internal/console/apiedgeref.go
+++ b/internal/console/apiedgeref.go
@@ -12,37 +12,30 @@ import (
"fmt"
"log"
"net/http"
- "strings"
"time"
"github.com/carlosframework/platform/internal/applog"
"github.com/carlosframework/platform/internal/reqrec"
)
-// apiEdgeRef is the wire shape of one explained code. Record is null when
-// the edge record is gone (sampled, evicted, or past retention); the seal
-// still tells the owner which rule fired and when.
+// apiEdgeRef is the wire shape of one explained code. The seal's fields
+// (code, verified, at, tier, rule_hash) are always there: they come from the
+// code itself, so they tell the owner which rule fired and when whatever
+// happened to the record. RecordStatus says what happened to it:
+//
+// found Record is the edge record carrying this code
+// absent the logs were read and hold no such record (sampled away,
+// evicted from the box's buffer, or past retention)
+// unavailable the logs could not be read (store outage, a bad chunk);
+// asking again later may find it
type apiEdgeRef struct {
- Code string `json:"code"`
- Verified bool `json:"verified"`
- At time.Time `json:"at"`
- Tier string `json:"tier"`
- RuleHash string `json:"rule_hash"`
- Record *applog.Record `json:"record"`
-}
-
-// canonicalRefCode is the form the edge writes into a record's ref field:
-// lower case, no spaces, no dashes. It deliberately does not fold the
-// look-alike letters the way ParseRef does, because the edge never writes
-// them; a pasted code that only parses by folding finds no record, and the
-// answer says so rather than guessing.
-func canonicalRefCode(code string) string {
- return strings.Map(func(r rune) rune {
- if r == ' ' || r == '-' {
- return -1
- }
- return r
- }, strings.ToLower(code))
+ Code string `json:"code"`
+ Verified bool `json:"verified"`
+ At time.Time `json:"at"`
+ Tier string `json:"tier"`
+ RuleHash string `json:"rule_hash"`
+ RecordStatus string `json:"record_status"`
+ Record *applog.Record `json:"record"`
}
// handleAPIEdgeRef answers GET /api/cli/apps/{account}/{app}/edge-refs/{code}.
@@ -67,7 +60,13 @@ func (s *Server) handleAPIEdgeRef(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
- canon := canonicalRefCode(code)
+ // ParseRef accepted it, so it has a canonical form: the spelling the edge
+ // wrote into the record, look-alike letters folded.
+ canon, err := reqrec.CanonicalRef(code)
+ if err != nil {
+ http.NotFound(w, r)
+ return
+ }
ctx, cancel := context.WithTimeout(r.Context(), instanceStoreTimeout)
defer cancel()
@@ -81,20 +80,23 @@ func (s *Server) handleAPIEdgeRef(w http.ResponseWriter, r *http.Request) {
Stream: applog.StreamEdge,
Fields: map[string]string{"ref": canon},
}, 1000)
- if err != nil {
- log.Printf("console: api edge ref %s/%s: %v", acct.Sqid, app, err)
- jsonError(w, http.StatusBadGateway, "could not read the logs")
- return
- }
resp := apiEdgeRef{
- Code: canon,
- Verified: ref.Verified,
- At: ref.At,
- Tier: ref.Tier.String(),
- RuleHash: fmt.Sprintf("%08x", ref.RuleHash),
+ Code: canon,
+ Verified: ref.Verified,
+ At: ref.At,
+ Tier: ref.Tier.String(),
+ RuleHash: fmt.Sprintf("%08x", ref.RuleHash),
+ RecordStatus: "absent",
}
- if len(recs) > 0 {
+ switch {
+ case err != nil:
+ // The code still explains itself; a failed read must not take that
+ // away (spec §7: a block stays explainable when its record is lost).
+ log.Printf("console: api edge ref %s/%s: %v", acct.Sqid, app, err)
+ resp.RecordStatus = "unavailable"
+ case len(recs) > 0:
resp.Record = &recs[0]
+ resp.RecordStatus = "found"
}
writeJSON(w, http.StatusOK, resp)
}
diff --git a/internal/console/apiedgeref_test.go b/internal/console/apiedgeref_test.go
index 2cb90c874..2a88a93a9 100644
--- a/internal/console/apiedgeref_test.go
+++ b/internal/console/apiedgeref_test.go
@@ -79,8 +79,8 @@ func TestEdgeRefFound(t *testing.T) {
if want := reqrec.RuleHash("probe-paths"); resp.RuleHash != fmt.Sprintf("%08x", want) {
t.Fatalf("rule_hash = %q, want %08x", resp.RuleHash, want)
}
- if resp.Record == nil || resp.Record.Fields["ref"] != code {
- t.Fatalf("record: %+v", resp.Record)
+ if resp.Record == nil || resp.Record.Fields["ref"] != code || resp.RecordStatus != "found" {
+ t.Fatalf("record %+v, status %q", resp.Record, resp.RecordStatus)
}
}
@@ -93,8 +93,8 @@ func TestEdgeRefRecordGone(t *testing.T) {
if status != http.StatusOK {
t.Fatalf("status = %d: %s", status, body)
}
- if !strings.Contains(body, `"record":null`) {
- t.Fatalf("want record null: %s", body)
+ if !strings.Contains(body, `"record":null`) || !strings.Contains(body, `"record_status":"absent"`) {
+ t.Fatalf("want record null and status absent: %s", body)
}
var resp apiEdgeRef
if err := json.Unmarshal([]byte(body), &resp); err != nil {
@@ -158,3 +158,52 @@ func TestEdgeRefPastedUpper(t *testing.T) {
t.Fatal("record not found for the pasted form")
}
}
+
+// TestEdgeRefLookAlikes: a code read out with o for 0 or i or l for 1, in
+// either case, verifies (ParseRef folds them) and so must also find its
+// record, and the answer names the code as the edge wrote it.
+func TestEdgeRefLookAlikes(t *testing.T) {
+ fx, token := edgeRefFixture(t, "code-edgeref-lookalike")
+ at := time.Now().UTC()
+ var code string
+ for i := 0; i < 1000 && !(strings.Contains(code, "0") && strings.Contains(code, "1")); i++ {
+ code = reqrec.NewRef(at, reqrec.TierProbe, "probe-paths", "box1", edgeRefTestKey)
+ }
+ if !strings.Contains(code, "0") || !strings.Contains(code, "1") {
+ t.Fatal("no code with both a 0 and a 1")
+ }
+ seedEdgeRecord(t, fx, code, at)
+ for _, sub := range [][2]string{{"0", "o"}, {"0", "O"}, {"1", "i"}, {"1", "I"}, {"1", "l"}, {"1", "L"}} {
+ pasted := strings.ReplaceAll(code, sub[0], sub[1])
+ resp := getEdgeRef(t, fx, token, pasted)
+ if !resp.Verified || resp.Code != code || resp.Record == nil || resp.RecordStatus != "found" {
+ t.Errorf("%s for %s (%s): code %q verified %v record %v status %q", sub[1], sub[0], pasted, resp.Code, resp.Verified, resp.Record != nil, resp.RecordStatus)
+ }
+ }
+}
+
+// TestEdgeRefRecordUnavailable: when the logs cannot be read, the code still
+// explains itself from its seal, and the answer says the record could not be
+// looked up rather than that it is gone.
+func TestEdgeRefRecordUnavailable(t *testing.T) {
+ fx, token := edgeRefFixture(t, "code-edgeref-unavailable")
+ at := time.Now().UTC()
+ code := reqrec.NewRef(at, reqrec.TierHard, "dotenv", "box1", edgeRefTestKey)
+ body := []byte("not a log chunk")
+ key := mustKey(t)(applog.ChunkKey(release.OpsAccount, "titogo", "box1", applog.HourOf(at), 900, len(body)))
+ if _, err := fx.rel.Put(context.Background(), key, body, ""); err != nil {
+ t.Fatal(err)
+ }
+ status, _, raw := rawReq(t, http.MethodGet, edgeRefURL(fx, fx.sqid, "titogo", code), token, "")
+ if status != http.StatusOK {
+ t.Fatalf("status = %d: %s", status, raw)
+ }
+ var resp apiEdgeRef
+ if err := json.Unmarshal([]byte(raw), &resp); err != nil {
+ t.Fatal(err)
+ }
+ if resp.RecordStatus != "unavailable" || resp.Record != nil || !resp.Verified || resp.Tier != "hard" ||
+ resp.RuleHash != fmt.Sprintf("%08x", reqrec.RuleHash("dotenv")) || resp.Code != code {
+ t.Fatalf("got %+v", resp)
+ }
+}
diff --git a/internal/console/svcroutes_test.go b/internal/console/svcroutes_test.go
index a1168244b..e9e2d61dc 100644
--- a/internal/console/svcroutes_test.go
+++ b/internal/console/svcroutes_test.go
@@ -563,9 +563,11 @@ var exemptFromLegacyPin = map[string]bool{
// about an address the caller chose.
"GET /api/cli/apps/{account}/{app}/geo": true,
// The edge reference code read (apiedgeref.go) is new access on the logs
- // read's footing: it reads the same app's edge records, and says nothing
- // about a code that does not verify. It did not exist before service
- // principals.
+ // read's footing: an authenticated, app-scoped read of the same app's
+ // edge records, behind the same instance gate and roles. An unsealed
+ // code, or a sealed one the console has no mesh key to check, decodes
+ // and is answered with verified:false; a forged or malformed code gets
+ // the gate's own 404. It did not exist before service principals.
"GET /api/cli/apps/{account}/{app}/edge-refs/{code}": true,
// The import door (seeded-instances spec §6.1) is new access on the
// create door's footing: every import call takes the instance role, as
diff --git a/internal/reqrec/ref.go b/internal/reqrec/ref.go
index 292d01497..e539dacc1 100644
--- a/internal/reqrec/ref.go
+++ b/internal/reqrec/ref.go
@@ -18,7 +18,7 @@ import (
// sink. Sealing it with the deployment's mesh key stops anyone minting a code
// that points support at a rule that never fired.
//
-// Layout, 20 bytes, then lower-case Crockford base32 without padding (32
+// Layout, 25 bytes, then lower-case Crockford base32 without padding (40
// characters, no leftover bits):
//
// 0 high nibble version (1), low nibble flags (bit 0 = sealed)
@@ -26,9 +26,15 @@ import (
// 5 tier
// 6-9 RuleHash
// 10-11 BoxHash
-// 12-14 random nonce, so two blocks in one minute get different codes
-// 15-19 first 5 bytes of HMAC-SHA256(key, "carlos-edge-ref\n" + bytes 0-14),
+// 12-19 random nonce, so two blocks in one minute get different codes
+// 20-24 first 5 bytes of HMAC-SHA256(key, "carlos-edge-ref\n" + bytes 0-19),
// zero when unsealed
+//
+// The nonce is 8 bytes because codes are not sampled: every block mints one,
+// and the console finds a record by its code. With 3 bytes, 300 blocks of one
+// rule in one minute on one box shared a nonce about once in 370 minutes,
+// and a shared code shows one request's record for another. With 8 the
+// chance is negligible at any rate a box can serve.
// Tier is the protection tier that produced a block.
type Tier uint8
@@ -69,10 +75,10 @@ var (
)
const (
- refLen = 20
+ refLen = 25
refVersion = 1
flagSealed = 1
- macOffset = 15
+ macOffset = 20
macLen = refLen - macOffset
refAlphabet = "0123456789abcdefghjkmnpqrstvwxyz"
@@ -117,7 +123,7 @@ func NewRef(at time.Time, tier Tier, ruleID, box string, key []byte) string {
binary.BigEndian.PutUint16(b[10:12], BoxHash(box))
// crypto/rand does not fail on supported platforms; if it ever did, a
// zero nonce only costs uniqueness, not correctness.
- _, _ = rand.Read(b[12:15])
+ _, _ = rand.Read(b[12:macOffset])
if len(key) > 0 {
copy(b[macOffset:], refMAC(key, b[:macOffset]))
}
@@ -126,6 +132,7 @@ func NewRef(at time.Time, tier Tier, ruleID, box string, key []byte) string {
// normalizeRef undoes what people do to a code on its way back to us: case,
// spacing, hyphens, and the look-alike letters Crockford folds together.
+// It is CanonicalRef without the shape check.
func normalizeRef(code string) string {
var sb strings.Builder
for _, c := range strings.ToLower(code) {
@@ -142,20 +149,35 @@ func normalizeRef(code string) string {
return sb.String()
}
+// CanonicalRef returns the one spelling of a pasted code that the edge
+// writes into a record's ref field: lower case, no spaces or hyphens, and
+// Crockford's look-alikes folded (o to 0, i and l to 1). ParseRef verifies
+// this form and the console looks records up by it, so a code that verifies
+// always finds its record. A string that cannot be a code (wrong length, a
+// letter outside the alphabet) is ErrBadRef; the version, tier and seal are
+// ParseRef's to judge.
+func CanonicalRef(code string) (string, error) {
+ s := normalizeRef(code)
+ if len(s) != refEncoding.EncodedLen(refLen) {
+ return "", ErrBadRef
+ }
+ if b, err := refEncoding.DecodeString(s); err != nil || len(b) != refLen {
+ return "", ErrBadRef
+ }
+ return s, nil
+}
+
// ParseRef decodes a code. With keys given, a sealed code must verify against
// one of them (current key first, then the next one during rotation) or it is
// ErrForgedRef. With no keys it decodes with Verified=false, which is how a
// reader without the mesh key still gets the rule and the time. Empty keys are
// ignored, so a caller passing a missing key gets the no-keys behaviour.
func ParseRef(code string, keys ...[]byte) (Ref, error) {
- s := normalizeRef(code)
- if len(s) != refEncoding.EncodedLen(refLen) {
- return Ref{}, ErrBadRef
- }
- b, err := refEncoding.DecodeString(s)
- if err != nil || len(b) != refLen {
- return Ref{}, ErrBadRef
+ s, err := CanonicalRef(code)
+ if err != nil {
+ return Ref{}, err
}
+ b, _ := refEncoding.DecodeString(s) // CanonicalRef decoded it already
if b[0]>>4 != refVersion || b[5] > byte(TierChallenge) {
return Ref{}, ErrBadRef
}
diff --git a/internal/reqrec/ref_test.go b/internal/reqrec/ref_test.go
index 4454532e4..a4e85474d 100644
--- a/internal/reqrec/ref_test.go
+++ b/internal/reqrec/ref_test.go
@@ -12,7 +12,7 @@ func TestRefRoundTrip(t *testing.T) {
key := bytes.Repeat([]byte{1}, 32)
at := time.Date(2026, 10, 24, 21, 7, 31, 0, time.UTC)
code := NewRef(at, TierProbe, "wp-login", "box-ie-1", key)
- if len(code) != 32 {
+ if len(code) != 40 {
t.Fatalf("len = %d", len(code))
}
r, err := ParseRef(code, key)
@@ -82,7 +82,7 @@ func TestRefBadTierAndVersion(t *testing.T) {
if _, err := ParseRef(raw(0x10, 1)); err != nil {
t.Errorf("valid unsealed: %v", err)
}
- if _, err := ParseRef(NewRef(time.Now(), TierHard, "r", "b", key)[:31], key); !errors.Is(err, ErrBadRef) {
+ if _, err := ParseRef(NewRef(time.Now(), TierHard, "r", "b", key)[:39], key); !errors.Is(err, ErrBadRef) {
t.Errorf("short code: %v", err)
}
// A tampered field on a sealed code fails the seal.
@@ -97,3 +97,82 @@ func TestRefBadTierAndVersion(t *testing.T) {
t.Errorf("tampered: %v", err)
}
}
+
+// TestRefNonceIsEightBytes: the nonce is bytes 12-19 and the seal covers all
+// of it, so two codes for one rule, tier, minute and box collide only by
+// chance in 64 bits, and changing any nonce byte breaks the seal.
+func TestRefNonceIsEightBytes(t *testing.T) {
+ key := bytes.Repeat([]byte{1}, 32)
+ at := time.Date(2026, 10, 24, 21, 7, 0, 0, time.UTC)
+ raw := func(code string) []byte {
+ b, err := refEncoding.DecodeString(code)
+ if err != nil || len(b) != 25 {
+ t.Fatalf("decode %q: %d bytes, %v", code, len(b), err)
+ }
+ return b
+ }
+ a, b := raw(NewRef(at, TierProbe, "r", "box", key)), raw(NewRef(at, TierProbe, "r", "box", key))
+ if !bytes.Equal(a[:12], b[:12]) {
+ t.Fatal("same rule, tier, minute and box gave different headers")
+ }
+ if bytes.Equal(a[12:20], b[12:20]) {
+ t.Fatal("two codes share a nonce")
+ }
+ for i := 12; i < 20; i++ {
+ c := append([]byte(nil), a...)
+ c[i] ^= 0x01
+ if _, err := ParseRef(encodeRef(c), key); !errors.Is(err, ErrForgedRef) {
+ t.Errorf("nonce byte %d changed: err = %v, want ErrForgedRef", i, err)
+ }
+ }
+}
+
+// TestCanonicalRef: every spelling ParseRef accepts has one canonical form,
+// the one the edge writes, so verification and record lookup agree on what
+// was pasted.
+func TestCanonicalRef(t *testing.T) {
+ key := bytes.Repeat([]byte{1}, 32)
+ code := codeWith(t, key, '0', '1')
+ variants := map[string]string{
+ "o for 0": strings.ReplaceAll(code, "0", "o"),
+ "O for 0": strings.ReplaceAll(code, "0", "O"),
+ "i for 1": strings.ReplaceAll(code, "1", "i"),
+ "I for 1": strings.ReplaceAll(code, "1", "I"),
+ "l for 1": strings.ReplaceAll(code, "1", "l"),
+ "L for 1": strings.ReplaceAll(code, "1", "L"),
+ "upper, dash": strings.ToUpper(code[:20] + "-" + code[20:]),
+ "spaces": " " + code[:10] + " " + code[10:] + " ",
+ }
+ for what, v := range variants {
+ got, err := CanonicalRef(v)
+ if err != nil || got != code {
+ t.Errorf("%s: CanonicalRef(%q) = %q, %v; want %q", what, v, got, err, code)
+ }
+ if r, err := ParseRef(v, key); err != nil || !r.Verified {
+ t.Errorf("%s: ParseRef: %+v %v", what, r, err)
+ }
+ }
+ for _, bad := range []string{"", "not-a-code", code[:39], code + "0", strings.Replace(code, code[:1], "u", 1)} {
+ if _, err := CanonicalRef(bad); !errors.Is(err, ErrBadRef) {
+ t.Errorf("CanonicalRef(%q): err = %v, want ErrBadRef", bad, err)
+ }
+ }
+}
+
+// codeWith mints sealed codes until one contains every byte in want, so a
+// look-alike substitution test has something to substitute.
+func codeWith(t *testing.T, key []byte, want ...byte) string {
+ t.Helper()
+ for i := 0; i < 1000; i++ {
+ code := NewRef(time.Now(), TierProbe, "probe-paths", "box1", key)
+ ok := true
+ for _, c := range want {
+ ok = ok && strings.IndexByte(code, c) >= 0
+ }
+ if ok {
+ return code
+ }
+ }
+ t.Fatalf("no code containing %q in 1000 tries", want)
+ return ""
+}