fix: improve UEPS reader error handling and test coverage
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
fe04cf93aa
commit
2561c6615d
2 changed files with 37 additions and 59 deletions
|
|
@ -112,10 +112,11 @@ func TestHMACVerification_TamperedHeader(t *testing.T) {
|
||||||
t.Fatalf("MarshalAndSign failed: %v", err)
|
t.Fatalf("MarshalAndSign failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tamper with the Version TLV value (byte index 2: tag=0, len=1, val=2)
|
// Tamper with the Version TLV value
|
||||||
|
// New Index: Tag (1 byte) + Length (2 bytes) = 3. Value is at index 3.
|
||||||
tampered := make([]byte, len(frame))
|
tampered := make([]byte, len(frame))
|
||||||
copy(tampered, frame)
|
copy(tampered, frame)
|
||||||
tampered[2] = 0x01 // Change version from 0x09 to 0x01
|
tampered[3] = 0x01 // Change version from 0x09 to 0x01
|
||||||
|
|
||||||
_, err = ReadAndVerify(bufio.NewReader(bytes.NewReader(tampered)), testSecret)
|
_, err = ReadAndVerify(bufio.NewReader(bytes.NewReader(tampered)), testSecret)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -206,9 +207,8 @@ func TestMissingHMACTag(t *testing.T) {
|
||||||
binary.BigEndian.PutUint16(tsBuf, 0)
|
binary.BigEndian.PutUint16(tsBuf, 0)
|
||||||
writeTLV(&buf, TagThreatScore, tsBuf)
|
writeTLV(&buf, TagThreatScore, tsBuf)
|
||||||
|
|
||||||
// Skip HMAC TLV entirely — go straight to payload
|
// Skip HMAC TLV entirely — go straight to payload (with length prefix now)
|
||||||
buf.WriteByte(TagPayload)
|
writeTLV(&buf, TagPayload, []byte("some data"))
|
||||||
buf.Write([]byte("some data"))
|
|
||||||
|
|
||||||
_, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(buf.Bytes())), testSecret)
|
_, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(buf.Bytes())), testSecret)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -221,10 +221,10 @@ func TestMissingHMACTag(t *testing.T) {
|
||||||
|
|
||||||
func TestWriteTLV_ValueTooLarge(t *testing.T) {
|
func TestWriteTLV_ValueTooLarge(t *testing.T) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
oversized := make([]byte, 256) // 1 byte over the 255 limit
|
oversized := make([]byte, 65536) // 1 byte over the 65535 limit
|
||||||
err := writeTLV(&buf, TagVersion, oversized)
|
err := writeTLV(&buf, TagVersion, oversized)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("Expected error for TLV value > 255 bytes")
|
t.Fatal("Expected error for TLV value > 65535 bytes")
|
||||||
}
|
}
|
||||||
if !strings.Contains(err.Error(), "TLV value too large") {
|
if !strings.Contains(err.Error(), "TLV value too large") {
|
||||||
t.Errorf("Expected 'TLV value too large' error, got: %v", err)
|
t.Errorf("Expected 'TLV value too large' error, got: %v", err)
|
||||||
|
|
@ -250,7 +250,7 @@ func TestTruncatedPacket(t *testing.T) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "CutInFirstTLVValue",
|
name: "CutInFirstTLVValue",
|
||||||
cutAt: 2, // Tag + length, but missing value
|
cutAt: 2, // Tag + partial length
|
||||||
wantErr: "EOF",
|
wantErr: "EOF",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -301,12 +301,11 @@ func TestUnknownTLVTag(t *testing.T) {
|
||||||
mac.Write(payload)
|
mac.Write(payload)
|
||||||
signature := mac.Sum(nil)
|
signature := mac.Sum(nil)
|
||||||
|
|
||||||
// Assemble full frame: headers + unknown + HMAC TLV + 0xFF + payload
|
// Assemble full frame: headers + unknown + HMAC TLV + 0xFF + payload (with length!)
|
||||||
var frame bytes.Buffer
|
var frame bytes.Buffer
|
||||||
frame.Write(headerBuf.Bytes())
|
frame.Write(headerBuf.Bytes())
|
||||||
writeTLV(&frame, TagHMAC, signature)
|
writeTLV(&frame, TagHMAC, signature)
|
||||||
frame.WriteByte(TagPayload)
|
writeTLV(&frame, TagPayload, payload)
|
||||||
frame.Write(payload)
|
|
||||||
|
|
||||||
parsed, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(frame.Bytes())), testSecret)
|
parsed, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(frame.Bytes())), testSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -387,9 +386,10 @@ func TestWriteTLV_BoundaryLengths(t *testing.T) {
|
||||||
}{
|
}{
|
||||||
{"Empty", 0, false},
|
{"Empty", 0, false},
|
||||||
{"OneByte", 1, false},
|
{"OneByte", 1, false},
|
||||||
{"MaxValid", 255, false},
|
{"OldMax", 255, false},
|
||||||
{"OneOver", 256, true},
|
{"NewMax", 65535, false},
|
||||||
{"WayOver", 1024, true},
|
{"OneOver", 65536, true},
|
||||||
|
{"WayOver", 100000, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
|
|
@ -407,6 +407,7 @@ func TestWriteTLV_BoundaryLengths(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TestReadAndVerify_EmptyReader verifies behaviour on completely empty input.
|
// TestReadAndVerify_EmptyReader verifies behaviour on completely empty input.
|
||||||
func TestReadAndVerify_EmptyReader(t *testing.T) {
|
func TestReadAndVerify_EmptyReader(t *testing.T) {
|
||||||
_, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(nil)), testSecret)
|
_, err := ReadAndVerify(bufio.NewReader(bytes.NewReader(nil)), testSecret)
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,12 @@ type ParsedPacket struct {
|
||||||
// It consumes the stream up to the end of the packet.
|
// It consumes the stream up to the end of the packet.
|
||||||
func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) {
|
func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error) {
|
||||||
// Buffer to reconstruct the data for HMAC verification
|
// Buffer to reconstruct the data for HMAC verification
|
||||||
// We have to "record" what we read to verify the signature later.
|
|
||||||
var signedData bytes.Buffer
|
var signedData bytes.Buffer
|
||||||
header := UEPSHeader{}
|
header := UEPSHeader{}
|
||||||
var signature []byte
|
var signature []byte
|
||||||
var payload []byte
|
var payload []byte
|
||||||
|
|
||||||
// Loop through TLVs until we hit Payload (0xFF) or EOF
|
// Loop through TLVs
|
||||||
for {
|
for {
|
||||||
// 1. Read Tag
|
// 1. Read Tag
|
||||||
tag, err := r.ReadByte()
|
tag, err := r.ReadByte()
|
||||||
|
|
@ -34,85 +33,64 @@ func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle Payload Tag (0xFF) - The Exit Condition
|
// 2. Read Length (2-byte big-endian uint16)
|
||||||
if tag == TagPayload {
|
lenBuf := make([]byte, 2)
|
||||||
// Stop recording signedData here (HMAC covers headers + payload, but logic splits)
|
if _, err := io.ReadFull(r, lenBuf); err != nil {
|
||||||
// Actually, wait. The HMAC covers (Headers + Payload).
|
|
||||||
// We need to read the payload to verify.
|
|
||||||
|
|
||||||
// For this implementation, we read until EOF or a specific delimiter?
|
|
||||||
// In a TCP stream, we need a length.
|
|
||||||
// If you are using standard TCP, you typically prefix the WHOLE frame with
|
|
||||||
// a 4-byte length. Assuming you handle that framing *before* calling this.
|
|
||||||
|
|
||||||
// Reading the rest as payload:
|
|
||||||
remaining, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
payload = remaining
|
|
||||||
|
|
||||||
// Add 0xFF and payload to the buffer for signature check?
|
|
||||||
// NO. In MarshalAndSign:
|
|
||||||
// mac.Write(buf.Bytes()) // Headers
|
|
||||||
// mac.Write(p.Payload) // Data
|
|
||||||
// It did NOT write the 0xFF tag into the HMAC.
|
|
||||||
|
|
||||||
break // Exit loop
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Read Length (Standard TLV)
|
|
||||||
lengthByte, err := r.ReadByte()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
length := int(lengthByte)
|
length := int(binary.BigEndian.Uint16(lenBuf))
|
||||||
|
|
||||||
// 4. Read Value
|
// 3. Read Value
|
||||||
value := make([]byte, length)
|
value := make([]byte, length)
|
||||||
if _, err := io.ReadFull(r, value); err != nil {
|
if _, err := io.ReadFull(r, value); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store for processing
|
// 4. Handle Tag
|
||||||
switch tag {
|
switch tag {
|
||||||
case TagVersion:
|
case TagVersion:
|
||||||
header.Version = value[0]
|
header.Version = value[0]
|
||||||
// Reconstruct signed data: Tag + Len + Val
|
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
case TagCurrentLay:
|
case TagCurrentLay:
|
||||||
header.CurrentLayer = value[0]
|
header.CurrentLayer = value[0]
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
case TagTargetLay:
|
case TagTargetLay:
|
||||||
header.TargetLayer = value[0]
|
header.TargetLayer = value[0]
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
case TagIntent:
|
case TagIntent:
|
||||||
header.IntentID = value[0]
|
header.IntentID = value[0]
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
case TagThreatScore:
|
case TagThreatScore:
|
||||||
header.ThreatScore = binary.BigEndian.Uint16(value)
|
header.ThreatScore = binary.BigEndian.Uint16(value)
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
case TagHMAC:
|
case TagHMAC:
|
||||||
signature = value
|
signature = value
|
||||||
// We do NOT add the HMAC itself to signedData
|
// HMAC tag itself is not part of the signed data
|
||||||
|
case TagPayload:
|
||||||
|
payload = value
|
||||||
|
// Exit loop after payload (last tag in UEPS frame)
|
||||||
|
// Note: The HMAC covers the Payload but NOT the TagPayload/Length bytes
|
||||||
|
// to match the PacketBuilder.MarshalAndSign logic.
|
||||||
|
goto verify
|
||||||
default:
|
default:
|
||||||
// Unknown tag (future proofing), verify it but ignore semantics
|
// Unknown tag (future proofing), verify it but ignore semantics
|
||||||
signedData.WriteByte(tag)
|
signedData.WriteByte(tag)
|
||||||
signedData.WriteByte(byte(length))
|
signedData.Write(lenBuf)
|
||||||
signedData.Write(value)
|
signedData.Write(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify:
|
||||||
if len(signature) == 0 {
|
if len(signature) == 0 {
|
||||||
return nil, errors.New("UEPS packet missing HMAC signature")
|
return nil, errors.New("UEPS packet missing HMAC signature")
|
||||||
}
|
}
|
||||||
|
|
@ -125,8 +103,6 @@ func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error)
|
||||||
expectedMAC := mac.Sum(nil)
|
expectedMAC := mac.Sum(nil)
|
||||||
|
|
||||||
if !hmac.Equal(signature, expectedMAC) {
|
if !hmac.Equal(signature, expectedMAC) {
|
||||||
// Log this. This is a Threat Event.
|
|
||||||
// "Axiom Violation: Integrity Check Failed"
|
|
||||||
return nil, errors.New("integrity violation: HMAC mismatch (ThreatScore +100)")
|
return nil, errors.New("integrity violation: HMAC mismatch (ThreatScore +100)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,3 +111,4 @@ func ReadAndVerify(r *bufio.Reader, sharedSecret []byte) (*ParsedPacket, error)
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue