Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/redaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,12 +496,12 @@ func (c *Client) buildOPRFMPCRanges() []*teeproto.OPRFRangeSpec {
}
tlsEnd++ // Include the end byte

// Validate length fits in single OPRF (max 64 bytes)
// Validate length fits in single OPRF (max 128 bytes)
tlsLength := tlsEnd - tlsStart
if tlsLength > 64 {
if tlsLength > 128 {
c.logger.Error("MPC OPRF range too long",
zap.Int("tls_length", tlsLength),
zap.Int("max", 64))
zap.Int("max", 128))
continue
Comment on lines +499 to 505
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fail fast instead of silently dropping provider-declared OPRF ranges.

If every requested range hits this branch, sendRedactionSpec() later sends an empty submission, so a hash:"oprf-mpc" redaction is downgraded to “no OPRF needed”. Please return an error here, or split the range explicitly, instead of continue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/redaction.go` around lines 499 - 505, The loop that checks OPRF range
length currently logs and continues when tlsLength > 128, which can silently
drop all provider-declared OPRF ranges; change this to fail fast by returning an
error instead of continuing. In the function that contains the loop (the code
path that eventually calls sendRedactionSpec), replace the "continue" after
logging with a returned error (e.g., fmt.Errorf or errors.Wrap) that includes
context such as tlsStart, tlsEnd, and tlsLength so callers can handle or surface
the failure; alternatively, if preferred, implement explicit splitting of the
range (using tlsStart/tlsEnd) into <=128-byte chunks before proceeding, but do
not leave the current "continue" behavior. Ensure the returned error propagates
up to prevent sendRedactionSpec from submitting an empty/no-OPRF redaction.

}
if tlsLength <= 0 {
Expand Down
4 changes: 2 additions & 2 deletions demo_lib/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ func main() {
"responseRedactions": []map[string]interface{}{
{
"xPath": "/html/body/footer/div[2]/div/div[1]/ul[3]/li[2]/a",
"regex": "href=\"https://(?<addr>www.trafficsafetymarketing.gov)/\"",
"regex": "(?<addr>.*)/\"",
"hash": "oprf-mpc",
},
},
Expand All @@ -246,7 +246,7 @@ func main() {
// - teetUrl: wss://tee-t.reclaimprotocol.org/ws (enclave mode)
// - attestorUrl: ws://localhost:8001/ws
configData := map[string]interface{}{
"attestorUrl": "ws://localhost:8001/ws", // Attestor WebSocket URL
"attestorUrl": "wss://attestor.reclaimprotocol.org:444/ws", // Attestor WebSocket URL
"teekUrl": "wss://tk.reclaimprotocol.org/ws",
"teetUrl": "wss://tt.reclaimprotocol.org/ws",
}
Expand Down
68 changes: 37 additions & 31 deletions oprfmpc/circuit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
// - Round 2: TEE_T -> TEE_K: CMAC result + hash output + output labels (MANDATORY)
//
// Properties:
// - TEE_K (Garbler) has: data share (up to 64 bytes), keyShareK (16 bytes)
// - TEE_T (Evaluator) has: data share (up to 64 bytes), keyShareT (16 bytes)
// - TEE_K (Garbler) has: data share (up to 128 bytes), keyShareK (16 bytes)
// - TEE_T (Evaluator) has: data share (up to 128 bytes), keyShareT (16 bytes)
// - Neither party sees: the combined key or plaintext
// - Output: 16-byte CMAC tag (both parties), then SHA256 offline for 32 bytes
package oprfmpc
Expand All @@ -33,16 +33,16 @@ import (
// aesCMACCircuit holds the compiled AES-CMAC OPRF circuit
var aesCMACCircuit *circuit.Circuit

// Input size: 64 bytes data + 16 bytes key = 80 bytes = 640 bits per party
const cmacInputBitCount = 640
// Input size: 128 bytes data + 16 bytes key = 144 bytes = 1152 bits per party
const cmacInputBitCount = 1152

// AESCMACResult holds the result of AES-CMAC OPRF computation
type AESCMACResult struct {
Output16 [16]byte // Raw 16-byte CMAC output
Output32 [32]byte // SHA256(CMAC output) for 32-byte result
}

// MPCL source for AES-CMAC OPRF with XOR-shared inputs
// MPCL source for AES-CMAC OPRF with XOR-shared inputs (128-byte data)
const aesCMACSource = `
package main

Expand All @@ -62,37 +62,43 @@ func leftShift(L [16]byte) [16]byte {
return result
}

func main(gInput [80]byte, eInput [80]byte) []byte {
func main(gInput [144]byte, eInput [144]byte) []byte {
var key [16]byte
for i := 0; i < 16; i++ {
key[i] = gInput[64+i] ^ eInput[64+i]
key[i] = gInput[128+i] ^ eInput[128+i]
}
var data [64]byte
for i := 0; i < 64; i++ {
var data [128]byte
for i := 0; i < 128; i++ {
data[i] = gInput[i] ^ eInput[i]
}
var zero [16]byte
L := aes.Block128(key, zero)
K1 := leftShift(L)
var M1, M2, M3, M4 [16]byte
var M1, M2, M3, M4, M5, M6, M7, M8 [16]byte
for i := 0; i < 16; i++ {
M1[i] = data[i]
M2[i] = data[16+i]
M3[i] = data[32+i]
M4[i] = data[48+i] ^ K1[i]
M4[i] = data[48+i]
M5[i] = data[64+i]
M6[i] = data[80+i]
M7[i] = data[96+i]
M8[i] = data[112+i] ^ K1[i]
}
C := aes.Block128(key, M1)
for i := 0; i < 16; i++ {
C[i] ^= M2[i]
}
for i := 0; i < 16; i++ { C[i] ^= M2[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ {
C[i] ^= M3[i]
}
for i := 0; i < 16; i++ { C[i] ^= M3[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ {
C[i] ^= M4[i]
}
for i := 0; i < 16; i++ { C[i] ^= M4[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ { C[i] ^= M5[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ { C[i] ^= M6[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ { C[i] ^= M7[i] }
C = aes.Block128(key, C)
for i := 0; i < 16; i++ { C[i] ^= M8[i] }
C = aes.Block128(key, C)
return C[:]
}
Expand All @@ -102,7 +108,7 @@ func init() {
params := utils.NewParams()
params.OptPruneGates = true
comp := compiler.New(params)
inputSizes := [][]int{{640}, {640}}
inputSizes := [][]int{{1152}, {1152}}
circ, _, err := comp.Compile(aesCMACSource, inputSizes)
if err != nil {
panic(fmt.Sprintf("failed to compile AES-CMAC OPRF circuit: %v", err))
Expand Down Expand Up @@ -140,15 +146,15 @@ var (
// CMACGarblerOnline creates the online phase payload using precomputed OT
// Parameters:
// - rng: randomness source
// - garblerInput: 80-byte input (64 bytes data + 16 bytes key share)
// - garblerInput: 144-byte input (128 bytes data + 16 bytes key share)
// - otEntries: precomputed OT entries from the pool (640 entries for 640 input bits)
// - otStartIndex: starting index in the OT pool (for tracking)
//
// Returns:
// - payload: the message to send to evaluator
// - session: state for verifying evaluator's output
// - err: any error
func CMACGarblerOnline(rng io.Reader, curve elliptic.Curve, garblerInput [80]byte, otEntries []*OTPoolEntry, otStartIndex int) (*CMACOnlinePayload, *CMACGarblerOnlineSession, error) {
func CMACGarblerOnline(rng io.Reader, curve elliptic.Curve, garblerInput [144]byte, otEntries []*OTPoolEntry, otStartIndex int) (*CMACOnlinePayload, *CMACGarblerOnlineSession, error) {
if rng == nil {
return nil, nil, errCMACNilRandom
}
Expand Down Expand Up @@ -247,13 +253,13 @@ func CMACGarblerOnline(rng io.Reader, curve elliptic.Curve, garblerInput [80]byt
// CMACEvaluatorOnline evaluates the garbled circuit using precomputed OT
// Parameters:
// - payload: the online payload from garbler
// - evaluatorInput: 80-byte input (64 bytes data + 16 bytes key share)
// - evaluatorInput: 144-byte input (128 bytes data + 16 bytes key share)
// - receiverEntries: precomputed OT receiver entries from the pool
//
// Returns:
// - result: CMAC output and output labels
// - err: any error
func CMACEvaluatorOnline(curve elliptic.Curve, payload *CMACOnlinePayload, evaluatorInput [80]byte, receiverEntries []*OTReceiverEntry) (*CMACOnlineResult, error) {
func CMACEvaluatorOnline(curve elliptic.Curve, payload *CMACOnlinePayload, evaluatorInput [144]byte, receiverEntries []*OTReceiverEntry) (*CMACOnlineResult, error) {
if payload == nil {
return nil, errors.New("nil payload")
}
Expand Down Expand Up @@ -396,15 +402,15 @@ func cmacBitsToBytes(bits []bool) []byte {
return bytes
}

// PadZeros64 pads data with zeros to exactly 64 bytes.
func PadZeros64(data []byte, dataLen int) ([64]byte, error) {
if dataLen > 64 {
return [64]byte{}, fmt.Errorf("dataLen too large: %d > 64", dataLen)
// PadZeros128 pads data with zeros to exactly 128 bytes.
func PadZeros128(data []byte, dataLen int) ([128]byte, error) {
if dataLen > 128 {
return [128]byte{}, fmt.Errorf("dataLen too large: %d > 128", dataLen)
}
if len(data) < dataLen {
return [64]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
return [128]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
}
var padded [64]byte
var padded [128]byte
copy(padded[:dataLen], data[:dataLen])
Comment on lines +405 to 414
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject negative dataLen.

dataLen < 0 passes the current guards and then panics on the slice expressions below. Returning an error keeps this helper safe even if a caller misses the upfront range validation.

🐛 Proposed fix
 func PadZeros128(data []byte, dataLen int) ([128]byte, error) {
-	if dataLen > 128 {
-		return [128]byte{}, fmt.Errorf("dataLen too large: %d > 128", dataLen)
+	if dataLen < 0 || dataLen > 128 {
+		return [128]byte{}, fmt.Errorf("invalid dataLen: %d", dataLen)
 	}
 	if len(data) < dataLen {
 		return [128]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// PadZeros128 pads data with zeros to exactly 128 bytes.
func PadZeros128(data []byte, dataLen int) ([128]byte, error) {
if dataLen > 128 {
return [128]byte{}, fmt.Errorf("dataLen too large: %d > 128", dataLen)
}
if len(data) < dataLen {
return [64]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
return [128]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
}
var padded [64]byte
var padded [128]byte
copy(padded[:dataLen], data[:dataLen])
// PadZeros128 pads data with zeros to exactly 128 bytes.
func PadZeros128(data []byte, dataLen int) ([128]byte, error) {
if dataLen < 0 || dataLen > 128 {
return [128]byte{}, fmt.Errorf("invalid dataLen: %d", dataLen)
}
if len(data) < dataLen {
return [128]byte{}, fmt.Errorf("data slice too short: %d < %d", len(data), dataLen)
}
var padded [128]byte
copy(padded[:dataLen], data[:dataLen])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oprfmpc/circuit.go` around lines 405 - 414, The PadZeros128 helper (function
PadZeros128) currently doesn't validate negative dataLen and will panic on slice
operations; add a guard at the start to reject dataLen < 0 (returning an error
like "dataLen negative: %d") so callers receive a clear error instead of a
runtime panic, keeping existing checks for dataLen > 128 and len(data) < dataLen
intact.

return padded, nil
}
Expand Down
69 changes: 38 additions & 31 deletions oprfmpc/circuit_crypto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,17 +416,24 @@ func TestCMAC_NISTVectors(t *testing.T) {
}

// Create inputs for the garbled circuit
// The circuit expects: gInput[64+i] XOR eInput[64+i] = key
// and: gInput[i] XOR eInput[i] = message
var garblerInput, evaluatorInput [80]byte
// The circuit expects: gInput[128+i] XOR eInput[128+i] = key
// and: gInput[i] XOR eInput[i] = message (128 bytes, zero-padded)
var garblerInput, evaluatorInput [144]byte

// Split the key: garbler has key, evaluator has zeros
copy(garblerInput[64:], key)
// evaluatorInput[64:] remains zeros
copy(garblerInput[128:], key)
// evaluatorInput[128:] remains zeros

// Split the message: garbler has message, evaluator has zeros
// Split the message: garbler has 64-byte message zero-padded to 128 bytes, evaluator has zeros
// Note: The remaining 64 bytes are zeros (CMAC of zero-padded message)
copy(garblerInput[:64], message)
// evaluatorInput[:64] remains zeros
// garblerInput[64:128] remains zeros (padding)
// evaluatorInput[:128] remains zeros

// For 128-byte input, we need to compute expected CMAC of the zero-padded message
var paddedMessage [128]byte
copy(paddedMessage[:64], message)
expectedCMAC = computeAESCMAC(key, paddedMessage[:])

// Run the garbled circuit OPRF
result, err := runCMACTest(t, garblerInput, evaluatorInput)
Expand All @@ -443,15 +450,15 @@ func TestCMAC_NISTVectors(t *testing.T) {
func TestCMAC_XORSharedInputs(t *testing.T) {
// Use random key and message, but XOR-share them between parties
key := make([]byte, 16)
message := make([]byte, 64)
message := make([]byte, 128)
rand.Read(key)
rand.Read(message)

// Random shares
keyShare1 := make([]byte, 16)
keyShare2 := make([]byte, 16)
msgShare1 := make([]byte, 64)
msgShare2 := make([]byte, 64)
msgShare1 := make([]byte, 128)
msgShare2 := make([]byte, 128)
rand.Read(keyShare1)
rand.Read(msgShare1)

Expand All @@ -465,11 +472,11 @@ func TestCMAC_XORSharedInputs(t *testing.T) {
}

// Build inputs
var garblerInput, evaluatorInput [80]byte
copy(garblerInput[:64], msgShare1)
copy(garblerInput[64:], keyShare1)
copy(evaluatorInput[:64], msgShare2)
copy(evaluatorInput[64:], keyShare2)
var garblerInput, evaluatorInput [144]byte
copy(garblerInput[:128], msgShare1)
copy(garblerInput[128:], keyShare1)
copy(evaluatorInput[:128], msgShare2)
copy(evaluatorInput[128:], keyShare2)

// Run the garbled circuit
result, err := runCMACTest(t, garblerInput, evaluatorInput)
Expand Down Expand Up @@ -520,7 +527,7 @@ func TestGarbledCircuit_EndToEnd(t *testing.T) {
}

// Create test inputs
var garblerInput, evaluatorInput [80]byte
var garblerInput, evaluatorInput [144]byte
rand.Read(garblerInput[:])
rand.Read(evaluatorInput[:])

Expand All @@ -545,11 +552,11 @@ func TestGarbledCircuit_EndToEnd(t *testing.T) {
// Verify the CMAC output is correct
// Reconstruct combined key and message
var combinedKey [16]byte
var combinedMsg [64]byte
var combinedMsg [128]byte
for i := 0; i < 16; i++ {
combinedKey[i] = garblerInput[64+i] ^ evaluatorInput[64+i]
combinedKey[i] = garblerInput[128+i] ^ evaluatorInput[128+i]
}
for i := 0; i < 64; i++ {
for i := 0; i < 128; i++ {
combinedMsg[i] = garblerInput[i] ^ evaluatorInput[i]
}

Expand Down Expand Up @@ -591,7 +598,7 @@ func TestGarbledCircuit_Soundness(t *testing.T) {
}
}

var garblerInput, evaluatorInput1, evaluatorInput2 [80]byte
var garblerInput, evaluatorInput1, evaluatorInput2 [144]byte
rand.Read(garblerInput[:])
rand.Read(evaluatorInput1[:])
copy(evaluatorInput2[:], evaluatorInput1[:])
Expand All @@ -616,12 +623,12 @@ func TestGarbledCircuit_Soundness(t *testing.T) {

// Compute expected CMACs
var combinedKey1, combinedKey2 [16]byte
var combinedMsg1, combinedMsg2 [64]byte
var combinedMsg1, combinedMsg2 [128]byte
for i := 0; i < 16; i++ {
combinedKey1[i] = garblerInput[64+i] ^ evaluatorInput1[64+i]
combinedKey2[i] = garblerInput[64+i] ^ evaluatorInput2[64+i]
combinedKey1[i] = garblerInput[128+i] ^ evaluatorInput1[128+i]
combinedKey2[i] = garblerInput[128+i] ^ evaluatorInput2[128+i]
}
for i := 0; i < 64; i++ {
for i := 0; i < 128; i++ {
combinedMsg1[i] = garblerInput[i] ^ evaluatorInput1[i]
combinedMsg2[i] = garblerInput[i] ^ evaluatorInput2[i]
}
Expand Down Expand Up @@ -661,7 +668,7 @@ func TestOutputLabelVerification_ValidLabels(t *testing.T) {
}
}

var garblerInput, evaluatorInput [80]byte
var garblerInput, evaluatorInput [144]byte
rand.Read(garblerInput[:])
rand.Read(evaluatorInput[:])

Expand Down Expand Up @@ -690,7 +697,7 @@ func TestOutputLabelVerification_InvalidLabels(t *testing.T) {
}
}

var garblerInput [80]byte
var garblerInput [144]byte
rand.Read(garblerInput[:])

_, session, _ := CMACGarblerOnline(rand.Reader, curve, garblerInput, otEntries, 0)
Expand Down Expand Up @@ -731,7 +738,7 @@ func TestOutputLabelVerification_ModifiedLabels(t *testing.T) {
}
}

var garblerInput, evaluatorInput [80]byte
var garblerInput, evaluatorInput [144]byte
rand.Read(garblerInput[:])
rand.Read(evaluatorInput[:])

Expand All @@ -749,7 +756,7 @@ func TestOutputLabelVerification_ModifiedLabels(t *testing.T) {
}

// Helper function to run CMAC test with given inputs
func runCMACTest(t *testing.T, garblerInput, evaluatorInput [80]byte) ([16]byte, error) {
func runCMACTest(t *testing.T, garblerInput, evaluatorInput [144]byte) ([16]byte, error) {
curve := elliptic.P256()

// Generate OT entries with precomputed choices matching evaluator's actual input bits
Expand Down Expand Up @@ -805,13 +812,13 @@ func computeAESCMAC(key, message []byte) []byte {
block.Encrypt(L, zero[:])
K1 := leftShift(L)

// Process 4 blocks (64 bytes)
// Process 8 blocks (128 bytes)
var C [16]byte
for blockIdx := 0; blockIdx < 4; blockIdx++ {
for blockIdx := 0; blockIdx < 8; blockIdx++ {
var M [16]byte
copy(M[:], message[blockIdx*16:(blockIdx+1)*16])

if blockIdx == 3 {
if blockIdx == 7 {
// XOR with K1 for last block (assuming complete block)
for i := range M {
M[i] ^= K1[i]
Expand Down
Loading