| // Copyright 2016 The Roughtime Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. */ | |
| package protocol | |
| import ( | |
| "bytes" | |
| "crypto/rand" | |
| "encoding/binary" | |
| "testing" | |
| "testing/quick" | |
| "golang.org/x/crypto/ed25519" | |
| ) | |
| func testEncodeDecodeRoundtrip(msg map[uint32][]byte) bool { | |
| encoded, err := Encode(msg) | |
| if err != nil { | |
| return true | |
| } | |
| decoded, err := Decode(encoded) | |
| if err != nil { | |
| return false | |
| } | |
| if len(msg) != len(decoded) { | |
| return false | |
| } | |
| for tag, payload := range msg { | |
| otherPayload, ok := decoded[tag] | |
| if !ok { | |
| return false | |
| } | |
| if !bytes.Equal(payload, otherPayload) { | |
| return false | |
| } | |
| } | |
| return true | |
| } | |
| func TestEncodeDecode(t *testing.T) { | |
| quick.Check(testEncodeDecodeRoundtrip, &quick.Config{ | |
| MaxCountScale: 10, | |
| }) | |
| } | |
| func TestRequestSize(t *testing.T) { | |
| _, _, request, err := CreateRequest(rand.Reader, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(request) != MinRequestSize { | |
| t.Errorf("got %d byte request, want %d bytes", len(request), MinRequestSize) | |
| } | |
| } | |
| func createServerIdentity(t *testing.T) (cert, rootPublicKey, onlinePrivateKey []byte) { | |
| rootPublicKey, rootPrivateKey, err := ed25519.GenerateKey(rand.Reader) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| onlinePublicKey, onlinePrivateKey, err := ed25519.GenerateKey(rand.Reader) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if cert, err = CreateCertificate(0, 100, onlinePublicKey, rootPrivateKey); err != nil { | |
| t.Fatal(err) | |
| } | |
| return cert, rootPublicKey, onlinePrivateKey | |
| } | |
| func TestRoundtrip(t *testing.T) { | |
| cert, rootPublicKey, onlinePrivateKey := createServerIdentity(t) | |
| for _, numRequests := range []int{1, 2, 3, 4, 5, 15, 16, 17} { | |
| nonces := make([][NonceSize]byte, numRequests) | |
| for i := range nonces { | |
| binary.LittleEndian.PutUint32(nonces[i][:], uint32(i)) | |
| } | |
| noncesSlice := make([][]byte, 0, numRequests) | |
| for i := range nonces { | |
| noncesSlice = append(noncesSlice, nonces[i][:]) | |
| } | |
| const ( | |
| expectedMidpoint = 50 | |
| expectedRadius = 5 | |
| ) | |
| replies, err := CreateReplies(noncesSlice, expectedMidpoint, expectedRadius, cert, onlinePrivateKey) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(replies) != len(nonces) { | |
| t.Fatalf("received %d replies for %d nonces", len(replies), len(nonces)) | |
| } | |
| for i, reply := range replies { | |
| midpoint, radius, err := VerifyReply(reply, rootPublicKey, nonces[i]) | |
| if err != nil { | |
| t.Errorf("error parsing reply #%d: %s", i, err) | |
| continue | |
| } | |
| if midpoint != expectedMidpoint { | |
| t.Errorf("reply #%d gave a midpoint of %d, want %d", i, midpoint, expectedMidpoint) | |
| } | |
| if radius != expectedRadius { | |
| t.Errorf("reply #%d gave a radius of %d, want %d", i, radius, expectedRadius) | |
| } | |
| } | |
| } | |
| } | |
| func TestChaining(t *testing.T) { | |
| // This test demonstrates how a claim of misbehaviour from a client | |
| // would be checked. The client creates a two element chain in this | |
| // example where the first server says that the time is 10 and the | |
| // second says that it's 5. | |
| certA, rootPublicKeyA, onlinePrivateKeyA := createServerIdentity(t) | |
| certB, rootPublicKeyB, onlinePrivateKeyB := createServerIdentity(t) | |
| nonce1, _, _, err := CreateRequest(rand.Reader, nil) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| replies1, err := CreateReplies([][]byte{nonce1[:]}, 10, 0, certA, onlinePrivateKeyA) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| nonce2, blind2, _, err := CreateRequest(rand.Reader, replies1[0]) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| replies2, err := CreateReplies([][]byte{nonce2[:]}, 5, 0, certB, onlinePrivateKeyB) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // The client would present a series of tuples of (server identity, | |
| // nonce/blind, reply) as its claim of misbehaviour. The first element | |
| // contains a nonce where as all other elements contain just the | |
| // blinding value, as the nonce used for that request is calculated | |
| // from that and the previous reply. | |
| type claimStep struct { | |
| serverPublicKey []byte | |
| nonceOrBlind [NonceSize]byte | |
| reply []byte | |
| } | |
| claim := []claimStep{ | |
| claimStep{rootPublicKeyA, nonce1, replies1[0]}, | |
| claimStep{rootPublicKeyB, blind2, replies2[0]}, | |
| } | |
| // In order to verify a claim, one would check each of the replies | |
| // based on the calculated nonce. | |
| var lastMidpoint uint64 | |
| var misbehaviourFound bool | |
| for i, step := range claim { | |
| var nonce [NonceSize]byte | |
| if i == 0 { | |
| copy(nonce[:], step.nonceOrBlind[:]) | |
| } else { | |
| nonce = CalculateChainNonce(claim[i-1].reply, step.nonceOrBlind[:]) | |
| } | |
| midpoint, _, err := VerifyReply(step.reply, step.serverPublicKey, nonce) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // This example doesn't take the radius into account. | |
| if i > 0 && midpoint < lastMidpoint { | |
| misbehaviourFound = true | |
| } | |
| lastMidpoint = midpoint | |
| } | |
| if !misbehaviourFound { | |
| t.Error("did not find expected misbehaviour") | |
| } | |
| } |