@@ -19,6 +19,7 @@ import (
1919 "encoding/hex"
2020 "fmt"
2121 "regexp"
22+ "strconv"
2223 "strings"
2324 "time"
2425
@@ -74,6 +75,33 @@ const (
7475 TokenRefreshCheckInterval = 10 * time .Minute
7576)
7677
78+ // EnrollChallenge is the payload a bootstrap enrollee signs to prove
79+ // possession of the private half of BootstrapEnrollRequest.public_key at
80+ // POST /enroll, carried in that message's timestamp/challenge_signature
81+ // fields. Binding the peer ID keeps a captured signature useless for any
82+ // other peer; the domain prefix keeps it useless at any other endpoint.
83+ func EnrollChallenge (peerID string , ts int64 ) []byte {
84+ return []byte ("sam:enroll:" + peerID + ":" + strconv .FormatInt (ts , 10 ))
85+ }
86+
87+ // EnrollStatusChallenge is the payload a bootstrap enrollee signs to prove
88+ // possession of the key it submitted at /enroll when polling
89+ // GET /enroll/status. ts is unix milliseconds; the signature travels in the
90+ // HeaderChallengeSignature header (unpadded base64url) alongside
91+ // HeaderChallengeTimestamp.
92+ func EnrollStatusChallenge (peerID string , ts int64 ) []byte {
93+ return []byte ("sam:enroll-status:" + peerID + ":" + strconv .FormatInt (ts , 10 ))
94+ }
95+
96+ // RefreshChallenge is the payload an enrolled peer signs to prove possession
97+ // of its identity key at POST /refresh, carried in TokenRefreshRequest's
98+ // timestamp/challenge_signature fields alongside the expiring biscuit. Same
99+ // shape as the other enrollment challenges: peer- and endpoint-bound, so a
100+ // captured signature is useless anywhere else.
101+ func RefreshChallenge (peerID string , ts int64 ) []byte {
102+ return []byte ("sam:refresh:" + peerID + ":" + strconv .FormatInt (ts , 10 ))
103+ }
104+
77105// ============================================================================
78106// SAM Custom HTTP Headers
79107// ============================================================================
@@ -87,6 +115,14 @@ const (
87115 // are forwarded to backend services.
88116 HeaderSamBiscuit = "X-Sam-Biscuit"
89117
118+ // HeaderChallengeTimestamp and HeaderChallengeSignature carry the signed
119+ // freshness challenge on GET /enroll/status: unix milliseconds and an
120+ // unpadded base64url signature over EnrollStatusChallenge. Headers rather
121+ // than query parameters, so the signature never lands in access logs,
122+ // where it would be replayable for its freshness window.
123+ HeaderChallengeTimestamp = "X-Sam-Challenge-Ts"
124+ HeaderChallengeSignature = "X-Sam-Challenge-Sig"
125+
90126 // HeaderPeerID carries the authenticated libp2p peer ID of the caller.
91127 // The mesh ingress handler stamps it after authorization succeeds,
92128 // overwriting any inbound value, so backend services get verified caller
0 commit comments