Skip to content

Commit 20bb52c

Browse files
authored
fix: three security vulnerabilities in Go and TypeScript verifiers
fix: three security vulnerabilities in Go and TypeScript verifiers
2 parents 630b6e2 + e804b18 commit 20bb52c

5 files changed

Lines changed: 46 additions & 14 deletions

File tree

go/shared/merkle/merkle.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package merkle
88

99
import (
1010
"crypto/sha256"
11+
"crypto/subtle"
1112
"errors"
1213
"fmt"
1314
)
@@ -145,10 +146,9 @@ func VerifyInclusion(entryHash []byte, entryIndex, treeSize int, proof [][]byte,
145146
size = (size + 1) / 2
146147
}
147148

148-
computed := fmt.Sprintf("%x", node)
149-
expected := fmt.Sprintf("%x", expectedRoot)
150-
if computed != expected {
151-
return fmt.Errorf("merkle: root mismatch: computed %s, expected %s", computed, expected)
149+
// subtle.ConstantTimeCompare prevents timing side-channels on the root hash.
150+
if subtle.ConstantTimeCompare(node, expectedRoot) != 1 {
151+
return fmt.Errorf("merkle: root mismatch: computed %x, expected %x", node, expectedRoot)
152152
}
153153
return nil
154154
}

go/verifier/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ func loadTrustConfigFromBytes(body []byte) error {
166166
copy(kid[:], kidBytes)
167167
witnesses[i] = verify.WitnessEntry{Name: wc.Name, KeyID: kid, PubKey: pubBytes}
168168
}
169+
if tc.WitnessQuorum < 1 {
170+
return fmt.Errorf("witness_quorum must be >= 1, got %d", tc.WitnessQuorum)
171+
}
172+
if tc.WitnessQuorum > len(witnesses) {
173+
return fmt.Errorf("witness_quorum (%d) exceeds witness count (%d)", tc.WitnessQuorum, len(witnesses))
174+
}
169175
v.AddAnchor(&verify.TrustAnchor{
170176
Origin: tc.Origin, OriginID: originIDInt, IssuerPubKey: issuerPubBytes,
171177
IssuerKeyName: tc.IssuerKeyName,

go/verifier/verify/verify.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,22 @@ type Result struct {
6868
SchemaID uint64 `json:"schema_id"`
6969
}
7070

71+
const maxCacheEntries = 1000
72+
7173
// Verifier holds trust anchors and a checkpoint cache.
7274
type Verifier struct {
73-
mu sync.RWMutex
74-
anchors map[uint64]*TrustAnchor
75-
cache map[string]*CachedCheckpoint
75+
mu sync.RWMutex
76+
anchors map[uint64]*TrustAnchor
77+
cache map[string]*CachedCheckpoint
78+
cacheOrder []string // insertion order for LRU eviction
7679
}
7780

7881
// New creates an empty Verifier.
7982
func New() *Verifier {
8083
return &Verifier{
81-
anchors: make(map[uint64]*TrustAnchor),
82-
cache: make(map[string]*CachedCheckpoint),
84+
anchors: make(map[uint64]*TrustAnchor),
85+
cache: make(map[string]*CachedCheckpoint),
86+
cacheOrder: make([]string, 0, maxCacheEntries+1),
8387
}
8488
}
8589

@@ -180,6 +184,13 @@ func (v *Verifier) Verify(payloadBytes []byte) *Result {
180184
anchor.WitnessQuorum, anchor.WitnessQuorum, fetchedSize))
181185
rootHash = fetchedRoot
182186
v.mu.Lock()
187+
if _, exists := v.cache[cacheKey]; !exists {
188+
if len(v.cacheOrder) >= maxCacheEntries {
189+
delete(v.cache, v.cacheOrder[0])
190+
v.cacheOrder = v.cacheOrder[1:]
191+
}
192+
v.cacheOrder = append(v.cacheOrder, cacheKey)
193+
}
183194
v.cache[cacheKey] = &CachedCheckpoint{TreeSize: fetchedSize, RootHash: fetchedRoot, FetchedAt: time.Now()}
184195
v.mu.Unlock()
185196
}

ts/shared/merkle.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { timingSafeEqual } from "crypto";
12
/**
23
* RFC 6962 §2.1 Merkle tree operations for MTA-QR.
34
* Leaf hashes: SHA-256(0x00 || data)
@@ -109,10 +110,11 @@ export function verifyInclusion(
109110
size = Math.floor((size + 1) / 2);
110111
}
111112

112-
const computed = Buffer.from(node).toString("hex");
113-
const expected = Buffer.from(expectedRoot).toString("hex");
114-
if (computed !== expected) {
115-
throw new Error(`merkle: root mismatch: computed ${computed}, expected ${expected}`);
113+
// timingSafeEqual prevents timing side-channels on the root hash comparison.
114+
if (!timingSafeEqual(Buffer.from(node), Buffer.from(expectedRoot))) {
115+
throw new Error(
116+
`merkle: root mismatch: computed ${Buffer.from(node).toString("hex")},` +
117+
` expected ${Buffer.from(expectedRoot).toString("hex")}`);
116118
}
117119
}
118120

ts/verifier/main.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ interface TrustAnchor {
2828
const anchors = new Map<string, TrustAnchor>(); // keyed by origin_id hex
2929

3030
// Checkpoint cache: "origin:treeSize" → rootHash
31+
// Bounded to prevent memory exhaustion from payloads with incrementing tree_size.
32+
const MAX_CACHE_ENTRIES = 1000;
3133
const checkpointCache = new Map<string, { rootHash: Uint8Array; fetchedAt: number }>();
3234

3335
// --- Verification ---
@@ -104,6 +106,9 @@ async function verify(payloadBytes: Uint8Array): Promise<VerifyResult> {
104106
add("Checkpoint fetch+verify", true,
105107
`issuer sig ✓ · ${anchor.witnessQuorum}/${anchor.witnessQuorum} witnesses ✓ · tree_size=${fetchedSize}`);
106108
rootHash = fetchedRoot;
109+
if (checkpointCache.size >= MAX_CACHE_ENTRIES) {
110+
checkpointCache.delete(checkpointCache.keys().next().value!);
111+
}
107112
checkpointCache.set(cacheKey, { rootHash: fetchedRoot, fetchedAt: Date.now() });
108113
}
109114

@@ -296,10 +301,18 @@ const server = createServer(async (req, res) => {
296301
keyID: new Uint8Array(Buffer.from(w.key_id_hex, "hex")),
297302
pubKey: new Uint8Array(Buffer.from(w.pub_key_hex, "hex")),
298303
}));
304+
const witnessQuorum = tc.witness_quorum;
305+
if (!Number.isInteger(witnessQuorum) || witnessQuorum < 1) {
306+
return json(res, { ok: false, error: `witness_quorum must be >= 1, got ${witnessQuorum}` }, 400);
307+
}
308+
if (witnessQuorum > witnesses.length) {
309+
return json(res, { ok: false,
310+
error: `witness_quorum (${witnessQuorum}) exceeds witness count (${witnesses.length})` }, 400);
311+
}
299312
const anchor: TrustAnchor = {
300313
origin: tc.origin, originID: oid, issuerPubKey,
301314
issuerKeyName: tc.issuer_key_name ?? "",
302-
sigAlg: tc.sig_alg, witnessQuorum: tc.witness_quorum,
315+
sigAlg: tc.sig_alg, witnessQuorum,
303316
witnesses, checkpointURL: tc.checkpoint_url,
304317
};
305318
anchors.set(oid.toString(16).padStart(16, "0"), anchor);

0 commit comments

Comments
 (0)