// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package tka import ( "bytes" "testing" "github.com/google/go-cmp/cmp" "golang.org/x/crypto/blake2s" "tailscale.com/types/tkatype" ) func TestSerialization(t *testing.T) { uint2 := uint(2) var fakeAUMHash AUMHash tcs := []struct { Name string AUM AUM Expect []byte }{ { "AddKey", AUM{MessageKind: AUMAddKey, Key: &Key{}}, []byte{ 0xa3, // major type 5 (map), 3 items 0x01, // |- major type 0 (int), value 1 (first key, MessageKind) 0x01, // |- major type 0 (int), value 1 (first value, AUMAddKey) 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash) 0xf6, // |- major type 7 (val), value null (second value, nil) 0x03, // |- major type 0 (int), value 3 (third key, Key) 0xa3, // |- major type 5 (map), 3 items (type Key) 0x01, // |- major type 0 (int), value 1 (first key, Kind) 0x00, // |- major type 0 (int), value 0 (first value) 0x02, // |- major type 0 (int), value 2 (second key, Votes) 0x00, // |- major type 0 (int), value 0 (first value) 0x03, // |- major type 0 (int), value 3 (third key, Public) 0xf6, // |- major type 7 (val), value null (third value, nil) }, }, { "RemoveKey", AUM{MessageKind: AUMRemoveKey, KeyID: []byte{1, 2}}, []byte{ 0xa3, // major type 5 (map), 3 items 0x01, // |- major type 0 (int), value 1 (first key, MessageKind) 0x02, // |- major type 0 (int), value 2 (first value, AUMRemoveKey) 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash) 0xf6, // |- major type 7 (val), value null (second value, nil) 0x04, // |- major type 0 (int), value 4 (third key, KeyID) 0x42, // |- major type 2 (byte string), 2 items 0x01, // |- major type 0 (int), value 1 (byte 1) 0x02, // |- major type 0 (int), value 2 (byte 2) }, }, { "UpdateKey", AUM{MessageKind: AUMUpdateKey, Votes: &uint2, KeyID: []byte{1, 2}, Meta: map[string]string{"a": "b"}}, []byte{ 0xa5, // major type 5 (map), 5 items 0x01, // |- major type 0 (int), value 1 (first key, MessageKind) 0x04, // |- major type 0 (int), value 4 (first value, AUMUpdateKey) 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash) 0xf6, // |- major type 7 (val), value null (second value, nil) 0x04, // |- major type 0 (int), value 4 (third key, KeyID) 0x42, // |- major type 2 (byte string), 2 items 0x01, // |- major type 0 (int), value 1 (byte 1) 0x02, // |- major type 0 (int), value 2 (byte 2) 0x06, // |- major type 0 (int), value 6 (fourth key, Votes) 0x02, // |- major type 0 (int), value 2 (forth value, 2) 0x07, // |- major type 0 (int), value 7 (fifth key, Meta) 0xa1, // |- major type 5 (map), 1 item (map[string]string type) 0x61, // |- major type 3 (text string), value 1 (first key, one byte long) 0x61, // |- byte 'a' 0x61, // |- major type 3 (text string), value 1 (first value, one byte long) 0x62, // |- byte 'b' }, }, { "Checkpoint", AUM{MessageKind: AUMCheckpoint, PrevAUMHash: []byte{1, 2}, State: &State{ LastAUMHash: &fakeAUMHash, Keys: []Key{ {Kind: Key25519, Public: []byte{5, 6}}, }, }}, append( append([]byte{ 0xa3, // major type 5 (map), 3 items 0x01, // |- major type 0 (int), value 1 (first key, MessageKind) 0x05, // |- major type 0 (int), value 5 (first value, AUMCheckpoint) 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash) 0x42, // |- major type 2 (byte string), 2 items (second value) 0x01, // |- major type 0 (int), value 1 (byte 1) 0x02, // |- major type 0 (int), value 2 (byte 2) 0x05, // |- major type 0 (int), value 5 (third key, State) 0xa3, // |- major type 5 (map), 3 items (third value, State type) 0x01, // |- major type 0 (int), value 1 (first key, LastAUMHash) 0x58, 0x20, // |- major type 2 (byte string), 32 items (first value) }, bytes.Repeat([]byte{0}, 32)...), []byte{ 0x02, // |- major type 0 (int), value 2 (second key, DisablementSecrets) 0xf6, // |- major type 7 (val), value null (second value, nil) 0x03, // |- major type 0 (int), value 3 (third key, Keys) 0x81, // |- major type 4 (array), value 1 (one item in array) 0xa3, // |- major type 5 (map), 3 items (Key type) 0x01, // |- major type 0 (int), value 1 (first key, Kind) 0x01, // |- major type 0 (int), value 1 (first value, Key25519) 0x02, // |- major type 0 (int), value 2 (second key, Votes) 0x00, // |- major type 0 (int), value 0 (second value, 0) 0x03, // |- major type 0 (int), value 3 (third key, Public) 0x42, // |- major type 2 (byte string), 2 items (third value) 0x05, // |- major type 0 (int), value 5 (byte 5) 0x06, // |- major type 0 (int), value 6 (byte 6) }...), }, { "Signature", AUM{MessageKind: AUMAddKey, Signatures: []tkatype.Signature{{KeyID: []byte{1}}}}, []byte{ 0xa3, // major type 5 (map), 3 items 0x01, // |- major type 0 (int), value 1 (first key, MessageKind) 0x01, // |- major type 0 (int), value 1 (first value, AUMAddKey) 0x02, // |- major type 0 (int), value 2 (second key, PrevAUMHash) 0xf6, // |- major type 7 (val), value null (second value, nil) 0x17, // |- major type 0 (int), value 22 (third key, Signatures) 0x81, // |- major type 4 (array), value 1 (one item in array) 0xa2, // |- major type 5 (map), 2 items (Signature type) 0x01, // |- major type 0 (int), value 1 (first key, KeyID) 0x41, // |- major type 2 (byte string), 1 item 0x01, // |- major type 0 (int), value 1 (byte 1) 0x02, // |- major type 0 (int), value 2 (second key, Signature) 0xf6, // |- major type 7 (val), value null (second value, nil) }, }, } for _, tc := range tcs { t.Run(tc.Name, func(t *testing.T) { data := []byte(tc.AUM.Serialize()) if diff := cmp.Diff(tc.Expect, data); diff != "" { t.Errorf("serialization differs (-want, +got):\n%s", diff) } var decodedAUM AUM if err := decodedAUM.Unserialize(data); err != nil { t.Fatalf("Unmarshal failed: %v", err) } if diff := cmp.Diff(tc.AUM, decodedAUM); diff != "" { t.Errorf("unmarshalled version differs (-want, +got):\n%s", diff) } }) } } func TestAUMWeight(t *testing.T) { var fakeKeyID [blake2s.Size]byte testingRand(t, 1).Read(fakeKeyID[:]) pub, _ := testingKey25519(t, 1) key := Key{Kind: Key25519, Public: pub, Votes: 2} pub, _ = testingKey25519(t, 2) key2 := Key{Kind: Key25519, Public: pub, Votes: 2} tcs := []struct { Name string AUM AUM State State Want uint }{ { "Empty", AUM{}, State{}, 0, }, { "Key unknown", AUM{ Signatures: []tkatype.Signature{{KeyID: fakeKeyID[:]}}, }, State{}, 0, }, { "Unary key", AUM{ Signatures: []tkatype.Signature{{KeyID: key.MustID()}}, }, State{ Keys: []Key{key}, }, 2, }, { "Multiple keys", AUM{ Signatures: []tkatype.Signature{{KeyID: key.MustID()}, {KeyID: key2.MustID()}}, }, State{ Keys: []Key{key, key2}, }, 4, }, { "Double use", AUM{ Signatures: []tkatype.Signature{{KeyID: key.MustID()}, {KeyID: key.MustID()}}, }, State{ Keys: []Key{key}, }, 2, }, } for _, tc := range tcs { t.Run(tc.Name, func(t *testing.T) { got := tc.AUM.Weight(tc.State) if got != tc.Want { t.Errorf("Weight() = %d, want %d", got, tc.Want) } }) } } func TestAUMHashes(t *testing.T) { // .Hash(): a hash over everything. // .SigHash(): a hash over everything except the signatures. // The signatures are over a hash of the AUM, so // using SigHash() breaks this circularity. aum := AUM{MessageKind: AUMAddKey, Key: &Key{Kind: Key25519}} sigHash1 := aum.SigHash() aumHash1 := aum.Hash() aum.Signatures = []tkatype.Signature{{KeyID: []byte{1, 2, 3, 4}}} sigHash2 := aum.SigHash() aumHash2 := aum.Hash() if len(aum.Signatures) != 1 { t.Error("signature was removed by one of the hash functions") } if !bytes.Equal(sigHash1[:], sigHash1[:]) { t.Errorf("signature hash dependent on signatures!\n\t1 = %x\n\t2 = %x", sigHash1, sigHash2) } if bytes.Equal(aumHash1[:], aumHash2[:]) { t.Error("aum hash didnt change") } }