Skip to content

Commit 31325d4

Browse files
committed
fix: enforce lexicographic ordering when encoding CBOR
Up to this point, we only had deterministic encoding for structs. Fields were encoded in the order they appeared. This fix ensures that fields are encoded in lexicographic order as required by CDE spec: https://www.ietf.org/archive/id/draft-ietf-cbor-cde-13.html#name-the-lexicographic-map-sorti (Note: we typically define fields in the order of their code points, so in practice, we have been _mostly_ compliant with CDE; but this was not guaranteed, and deviations were possible, especially when extensions are involved.) Signed-off-by: Sergei Trofimov <[email protected]>
1 parent 32c5cbc commit 31325d4

File tree

2 files changed

+106
-1
lines changed

2 files changed

+106
-1
lines changed

encoding/cbor.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"math"
1111
"reflect"
12+
"sort"
1213
"strconv"
1314
"strings"
1415

@@ -285,6 +286,7 @@ func (o *structFieldsCBOR) ToCBOR(em cbor.EncMode) ([]byte, error) {
285286
return nil, errors.New("mapLen cannot exceed math.MaxUint32")
286287
}
287288

289+
lexSort(em, o.Keys)
288290
for _, key := range o.Keys {
289291
marshalledKey, err := em.Marshal(key)
290292
if err != nil {
@@ -500,3 +502,29 @@ func updateFieldCacheCBOR(dm cbor.DecMode, cacheField reflect.Value, rawMap *str
500502

501503
return nil
502504
}
505+
506+
// Lexicographic sorting of CBOR integer keys. See:
507+
// https://www.ietf.org/archive/id/draft-ietf-cbor-cde-13.html#name-the-lexicographic-map-sorti
508+
func lexSort(em cbor.EncMode, v []int) {
509+
sort.Slice(v, func(i, j int) bool {
510+
a, err := em.Marshal(v[i])
511+
if err != nil {
512+
panic(err) // integer encoding cannot fail
513+
}
514+
515+
b, err := em.Marshal(v[j])
516+
if err != nil {
517+
panic(err) // integer encoding cannot fail
518+
}
519+
520+
for k, v := range a {
521+
if v < b[k] {
522+
return true
523+
} else if v > b[k] {
524+
return false
525+
}
526+
}
527+
528+
return false
529+
})
530+
}

encoding/cbor_test.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2021-2024 Contributors to the Veraison project.
1+
// Copyright 2021-2025 Contributors to the Veraison project.
22
// SPDX-License-Identifier: Apache-2.0
33
package encoding
44

@@ -107,6 +107,41 @@ func Test_PopulateStructFromCBOR_simple(t *testing.T) {
107107

108108
}
109109

110+
func Test_SerializeStructToCBOR_cde_ordering(t *testing.T) {
111+
val := struct {
112+
Field8 int `cbor:"8,keyasint"`
113+
FieldN3 int `cbor:"-3,keyasint"`
114+
Field0 int `cbor:"0,keyasint"`
115+
FieldN1 int `cbor:"-1,keyasint"`
116+
}{
117+
Field8: 1,
118+
FieldN3: 3,
119+
Field0: 0,
120+
FieldN1: 2,
121+
}
122+
123+
expected := []byte{
124+
0xa4, // map(4)
125+
126+
0x00, // key: 0
127+
0x00, // value: 0
128+
129+
0x08, // key: 8
130+
0x01, // value: 1
131+
132+
0x20, // key: -1
133+
0x02, // value: 2
134+
135+
0x22, // key: -3
136+
0x03, // value: 3
137+
}
138+
139+
em := mustInitEncMode()
140+
data, err := SerializeStructToCBOR(em, val)
141+
assert.NoError(t, err)
142+
assert.Equal(t, expected, data)
143+
}
144+
110145
func Test_structFieldsCBOR_CRUD(t *testing.T) {
111146
sf := newStructFieldsCBOR()
112147

@@ -278,3 +313,45 @@ func Test_processAdditionalInfo(t *testing.T) {
278313
_, _, err = processAdditionalInfo(addInfo, []byte{})
279314
assert.EqualError(t, err, "unexpected EOF")
280315
}
316+
317+
func Test_lexSort(t *testing.T) {
318+
test_cases := []struct {
319+
title string
320+
input []int
321+
expected []int
322+
}{
323+
{
324+
title: "non-negative",
325+
input: []int{1, 4, 0, 2, 3},
326+
expected: []int{0, 1, 2, 3, 4},
327+
},
328+
{
329+
title: "negative",
330+
input: []int{-1, -4, -2, -3},
331+
expected: []int{-1, -2, -3, -4},
332+
},
333+
{
334+
title: "mixed",
335+
input: []int{-1, 0, 3, 1, -2},
336+
expected: []int{0, 1, 3, -1, -2},
337+
},
338+
{
339+
title: "already sorted",
340+
input: []int{0, 1, 3, -1, -2},
341+
expected: []int{0, 1, 3, -1, -2},
342+
},
343+
{
344+
title: "different length encoding",
345+
input: []int{65535, 256},
346+
expected: []int{256, 65535},
347+
},
348+
}
349+
350+
for _, tc := range test_cases {
351+
em := mustInitEncMode()
352+
t.Run(tc.title, func(t *testing.T) {
353+
lexSort(em, tc.input)
354+
assert.Equal(t, tc.expected, tc.input)
355+
})
356+
}
357+
}

0 commit comments

Comments
 (0)