Skip to content

Commit

Permalink
Add shuffled SortMode for encoding struct fields less predictably.
Browse files Browse the repository at this point in the history
The motivation is to prevent programs that consume the output from assuming (incorrectly) that it
was encoded deterministically, even when implementation details make the output apparently stable.

Signed-off-by: Ben Luddy <[email protected]>
  • Loading branch information
benluddy committed Apr 12, 2024
1 parent 9f099e8 commit 5114ddb
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 10 deletions.
9 changes: 5 additions & 4 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,13 +174,14 @@ type encodingStructType struct {
}

func (st *encodingStructType) getFields(em *encMode) fields {
if em.sort == SortNone {
switch em.sort {
case SortNone, SortFastShuffle:
return st.fields
}
if em.sort == SortLengthFirst {
case SortLengthFirst:
return st.lengthFirstFields
default:
return st.bytewiseFields
}
return st.bytewiseFields
}

type bytewiseFieldSorter struct {
Expand Down
36 changes: 30 additions & 6 deletions encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"math"
"math/big"
"math/rand"
"reflect"
"sort"
"strconv"
Expand Down Expand Up @@ -141,7 +142,7 @@ func (e *UnsupportedValueError) Error() string {
type SortMode int

const (
// SortNone means no sorting.
// SortNone encodes map pairs and struct fields in an arbitrary order.
SortNone SortMode = 0

// SortLengthFirst causes map keys or struct fields to be sorted such that:
Expand All @@ -157,6 +158,12 @@ const (
// in RFC 7049bis.
SortBytewiseLexical SortMode = 2

// SortShuffle encodes map pairs and struct fields in a shuffled
// order. This mode does not guarantee an unbiased permutation, but it
// does guarantee that the runtime of the shuffle algorithm used will be
// constant.
SortFastShuffle SortMode = 3

// SortCanonical is used in "Canonical CBOR" encoding in RFC 7049 3.9.
SortCanonical SortMode = SortLengthFirst

Expand All @@ -166,7 +173,7 @@ const (
// SortCoreDeterministic is used in "Core Deterministic Encoding" in RFC 7049bis.
SortCoreDeterministic SortMode = SortBytewiseLexical

maxSortMode SortMode = 3
maxSortMode SortMode = 4
)

func (sm SortMode) valid() bool {
Expand Down Expand Up @@ -1081,8 +1088,12 @@ func (me mapEncodeFunc) encode(e *encoderBuffer, em *encMode, v reflect.Value) e
if mlen == 0 {
return e.WriteByte(byte(cborTypeMap))
}
if em.sort != SortNone && mlen > 1 {
return me.encodeCanonical(e, em, v)
switch em.sort {
case SortNone, SortFastShuffle:
default:
if mlen > 1 {
return me.encodeCanonical(e, em, v)
}
}
encodeHead(e, byte(cborTypeMap), uint64(mlen))

Expand Down Expand Up @@ -1234,7 +1245,13 @@ func encodeFixedLengthStruct(e *encoderBuffer, em *encMode, v reflect.Value, fld

encodeHead(e, byte(cborTypeMap), uint64(len(flds)))

for i := 0; i < len(flds); i++ {
start := 0
if em.sort == SortFastShuffle {
start = rand.Intn(len(flds))

Check failure on line 1250 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
}

for offset := 0; offset < len(flds); offset++ {
i := (start + offset) % len(flds)
f := flds[i]
if !f.keyAsInt && em.fieldName == FieldNameToByteString {
e.Write(f.cborNameByteString)
Expand Down Expand Up @@ -1263,9 +1280,16 @@ func encodeStruct(e *encoderBuffer, em *encMode, v reflect.Value) (err error) {
return encodeFixedLengthStruct(e, em, v, flds)
}

start := 0
if em.sort == SortFastShuffle {
start = rand.Intn(len(flds))

Check failure on line 1285 in encode.go

View workflow job for this annotation

GitHub Actions / Lint

G404: Use of weak random number generator (math/rand instead of crypto/rand) (gosec)
}

kve := getEncoderBuffer() // encode key-value pairs based on struct field tag options
kvcount := 0
for i := 0; i < len(flds); i++ {

for offset := 0; offset < len(flds); offset++ {
i := (start + offset) % len(flds)
f := flds[i]

var fv reflect.Value
Expand Down
59 changes: 59 additions & 0 deletions encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4343,3 +4343,62 @@ func TestMarshalerReturnsDisallowedCBORData(t *testing.T) {
})
}
}

func TestSortModeFastShuffle(t *testing.T) {
type FixedLength struct {

Check failure on line 4348 in encode_test.go

View workflow job for this annotation

GitHub Actions / Lint

type `FixedLength` is unused (unused)
A, B int
}

type VariableLength struct {

Check failure on line 4352 in encode_test.go

View workflow job for this annotation

GitHub Actions / Lint

type `VariableLength` is unused (unused)
A int
B int `cbor:",omitempty"`
}

em, err := EncOptions{Sort: SortFastShuffle}.EncMode()
if err != nil {
t.Fatal(err)
}

// These cases are based on the assumption that even a constant-time shuffle algorithm can
// give an unbiased permutation of the keys when there are exactly 2 keys, so each trial
// should succeed with probability 1/2.

for _, tc := range []struct {
name string
trials int
in interface{}
}{
{
name: "fixed length struct",
trials: 1024,
in: struct{ A, B int }{},
},
{
name: "variable length struct",
trials: 1024,
in: struct {
A int
B int `cbor:",omitempty"`
}{B: 1},
},
} {
t.Run(tc.name, func(t *testing.T) {
first, err := em.Marshal(tc.in)
if err != nil {
t.Fatal(err)
}

for i := 1; i <= tc.trials; i++ {
next, err := em.Marshal(tc.in)
if err != nil {
t.Fatal(err)
}
if string(first) != string(next) {
return
}
}

t.Errorf("object encoded identically in %d consecutive trials using SortFastShuffle", tc.trials)
})
}
}

0 comments on commit 5114ddb

Please sign in to comment.