Skip to content

Commit 5125e82

Browse files
l33tdawgclaude
andcommitted
fix: thread agent nonce through tx codec and ABCI verification
The SDK's X-Nonce header was verified at the REST middleware layer but dropped before reaching CometBFT ABCI consensus. The nonce was missing from AgentAuthProof, ParsedTx, the wire codec, and VerifyAgentProof, causing ABCI FinalizeBlock to reconstruct bodyHash+timestamp (without nonce) while the signature covered bodyHash+timestamp+nonce — resulting in "agent signature verification failed" (code 11) on every submit. Threads AgentNonce through the full chain: middleware → embedAgentAuth → tx codec (length-prefixed, backward compatible) → VerifyAgentProof. Legacy transactions without nonce continue to verify correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 310a47d commit 5125e82

6 files changed

Lines changed: 33 additions & 7 deletions

File tree

api/rest/middleware/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type AgentAuthProof struct {
6666
Signature []byte // Ed25519 signature (64 bytes)
6767
Timestamp int64 // Unix seconds used in signing
6868
BodyHash []byte // SHA-256 of request body (32 bytes)
69+
Nonce []byte // Optional nonce used in signing (nil if legacy)
6970
}
7071

7172
// skipAuthPaths lists paths that bypass authentication.
@@ -204,6 +205,7 @@ func Ed25519AuthMiddleware(next http.Handler) http.Handler {
204205
Signature: sig,
205206
Timestamp: tsUnix,
206207
BodyHash: bodyHash[:],
208+
Nonce: nonce,
207209
}
208210
ctx := context.WithValue(r.Context(), agentIDKey, agentID)
209211
ctx = context.WithValue(ctx, agentAuthKey, proof)

api/rest/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ func embedAgentAuth(ctx context.Context, ptx *tx.ParsedTx) {
276276
ptx.AgentSig = proof.Signature
277277
ptx.AgentTimestamp = proof.Timestamp
278278
ptx.AgentBodyHash = proof.BodyHash
279+
ptx.AgentNonce = proof.Nonce
279280
}
280281

281282
// Router returns the underlying chi router for testing.

internal/abci/app.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,7 +1113,7 @@ func verifyAgentIdentity(parsedTx *tx.ParsedTx) (string, error) {
11131113
if len(parsedTx.AgentPubKey) == 0 || len(parsedTx.AgentSig) == 0 || len(parsedTx.AgentBodyHash) == 0 {
11141114
return "", fmt.Errorf("no agent identity proof in transaction")
11151115
}
1116-
return auth.VerifyAgentProof(parsedTx.AgentPubKey, parsedTx.AgentSig, parsedTx.AgentBodyHash, parsedTx.AgentTimestamp)
1116+
return auth.VerifyAgentProof(parsedTx.AgentPubKey, parsedTx.AgentSig, parsedTx.AgentBodyHash, parsedTx.AgentTimestamp, parsedTx.AgentNonce)
11171117
}
11181118

11191119
func (app *SageApp) processOrgRegister(parsedTx *tx.ParsedTx, height int64, blockTime time.Time) *abcitypes.ExecTxResult {

internal/auth/ed25519.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func buildRequestMessage(method, path string, body []byte, timestamp int64, nonc
100100
//
101101
// This is the critical on-chain identity verification — the ABCI handler uses
102102
// this to independently establish agent identity without trusting the REST layer.
103-
func VerifyAgentProof(agentPubKey, agentSig, bodyHash []byte, agentTimestamp int64) (string, error) {
103+
func VerifyAgentProof(agentPubKey, agentSig, bodyHash []byte, agentTimestamp int64, agentNonce []byte) (string, error) {
104104
if len(agentPubKey) != ed25519.PublicKeySize {
105105
return "", fmt.Errorf("invalid agent public key length: %d", len(agentPubKey))
106106
}
@@ -123,12 +123,15 @@ func VerifyAgentProof(agentPubKey, agentSig, bodyHash []byte, agentTimestamp int
123123
return "", fmt.Errorf("no agent identity proof in transaction")
124124
}
125125

126-
// Reconstruct the signed message: bodyHash + bigEndian(timestamp)
126+
// Reconstruct the signed message: bodyHash + bigEndian(timestamp) [+ nonce]
127127
tsBytes := make([]byte, 8)
128128
binary.BigEndian.PutUint64(tsBytes, uint64(agentTimestamp)) // #nosec G115 -- timestamp from trusted int64
129-
message := make([]byte, 0, 40)
129+
message := make([]byte, 0, 40+len(agentNonce))
130130
message = append(message, bodyHash...)
131131
message = append(message, tsBytes...)
132+
if len(agentNonce) > 0 {
133+
message = append(message, agentNonce...)
134+
}
132135

133136
// Verify the agent's Ed25519 signature
134137
if !ed25519.Verify(ed25519.PublicKey(agentPubKey), message, agentSig) {

internal/tx/codec.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,15 @@ func EncodeTx(tx *ParsedTx) ([]byte, error) {
5050
agentBodyHash = make([]byte, 32)
5151
}
5252

53+
agentNonce := tx.AgentNonce
54+
if agentNonce == nil {
55+
agentNonce = []byte{}
56+
}
57+
5358
// type(1) + payloadLen(4) + payload + nodeSig(64) + nodePub(32) + nonce(8) + timestamp(8)
54-
// + agentPub(32) + agentSig(64) + agentTs(8) + agentBodyHash(32)
59+
// + agentPub(32) + agentSig(64) + agentTs(8) + agentBodyHash(32) + agentNonceLen(4) + agentNonce(var)
5560
totalLen := 1 + 4 + len(payload) + ed25519.SignatureSize + ed25519.PublicKeySize + 8 + 8 +
56-
ed25519.PublicKeySize + ed25519.SignatureSize + 8 + 32
61+
ed25519.PublicKeySize + ed25519.SignatureSize + 8 + 32 + 4 + len(agentNonce)
5762
buf := make([]byte, 0, totalLen)
5863
buf = append(buf, byte(tx.Type))
5964
buf = appendUint32(buf, uint32(len(payload))) // #nosec G115 -- payload length fits in uint32
@@ -67,6 +72,9 @@ func EncodeTx(tx *ParsedTx) ([]byte, error) {
6772
buf = append(buf, agentSig...)
6873
buf = appendInt64(buf, tx.AgentTimestamp)
6974
buf = append(buf, agentBodyHash...)
75+
// Agent nonce (variable length, length-prefixed)
76+
buf = appendUint32(buf, uint32(len(agentNonce))) // #nosec G115 -- nonce length fits in uint32
77+
buf = append(buf, agentNonce...)
7078

7179
return buf, nil
7280
}
@@ -132,6 +140,17 @@ func DecodeTx(data []byte) (*ParsedTx, error) {
132140

133141
tx.AgentBodyHash = make([]byte, 32)
134142
copy(tx.AgentBodyHash, data[offset:offset+32])
143+
offset += 32
144+
145+
// Decode agent nonce if present (length-prefixed, variable length)
146+
if offset+4 <= len(data) {
147+
nonceLen := binary.BigEndian.Uint32(data[offset : offset+4])
148+
offset += 4
149+
if nonceLen > 0 && offset+int(nonceLen) <= len(data) { // #nosec G115 -- nonceLen validated
150+
tx.AgentNonce = make([]byte, nonceLen)
151+
copy(tx.AgentNonce, data[offset:offset+int(nonceLen)]) // #nosec G115 -- nonceLen validated
152+
}
153+
}
135154
}
136155

137156
if err := decodePayload(tx, payload); err != nil {

internal/tx/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,11 +282,12 @@ type ParsedTx struct {
282282
Timestamp time.Time
283283

284284
// Agent identity proof — allows ABCI to verify agent identity on-chain.
285-
// The agent signed SHA256(requestBody) + bigEndian(AgentTimestamp) with their key.
285+
// The agent signed SHA256(requestBody) + bigEndian(AgentTimestamp) [+ nonce] with their key.
286286
// ABCI re-verifies this signature to establish the authenticated agent identity
287287
// independently of the REST layer.
288288
AgentPubKey []byte // Agent Ed25519 public key (32 bytes)
289289
AgentSig []byte // Agent Ed25519 signature (64 bytes)
290290
AgentTimestamp int64 // Unix seconds timestamp used in signing
291291
AgentBodyHash []byte // SHA256 of original request body (32 bytes)
292+
AgentNonce []byte // Optional nonce used in signing (variable length, 0 if legacy)
292293
}

0 commit comments

Comments
 (0)