carlos / movement Public

Clone
git clone https://amadan.net/carlos/movement

Plain git — no account needed to clone.

Download

Download this file

1package movement
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8)
9
10func testKey() []byte { return ChallengeKey(strings.Repeat("k", 64)) }
11
12func TestFreshSealVerifies(t *testing.T) {
13 key, now := testKey(), time.Now()
14 c := NewChallenge(key, now, DefaultDifficulty)
15
16 if reason, ok := VerifySeal(key, c, now.Add(10*time.Second)); !ok {
17 t.Fatalf("a fresh seal must verify, got %q", reason)
18 }
19}
20
21func TestSealRejectsOutsideItsWindow(t *testing.T) {
22 key, now := testKey(), time.Now()
23 c := NewChallenge(key, now, DefaultDifficulty)
24
25 if r, _ := VerifySeal(key, c, now.Add(time.Second)); r != ReasonTooFast {
26 t.Errorf("a submission one second after the page rendered is not a person: got %q", r)
27 }
28 if r, _ := VerifySeal(key, c, now.Add(3*time.Hour)); r != ReasonTooOld {
29 t.Errorf("a three-hour-old challenge must be refused: got %q", r)
30 }
31}
32
33// Every field is covered by the signature, so none of them can be edited on
34// the way back. Difficulty matters most: unsealed, a submitter would simply
35// lower it and skip the work.
36func TestSealCoversEveryField(t *testing.T) {
37 key, now := testKey(), time.Now()
38 at := now.Add(10 * time.Second)
39 c := NewChallenge(key, now, DefaultDifficulty)
40
41 tampered := map[string]Challenge{
42 "difficulty": {c.Nonce, c.IssuedAt, 1, c.Seal},
43 "issuedAt": {c.Nonce, c.IssuedAt - 3600, c.Difficulty, c.Seal},
44 "nonce": {"deadbeef", c.IssuedAt, c.Difficulty, c.Seal},
45 "seal": {c.Nonce, c.IssuedAt, c.Difficulty, strings.Repeat("0", 64)},
46 }
47 for field, bad := range tampered {
48 if r, ok := VerifySeal(key, bad, at); ok || r != ReasonSealInvalid {
49 t.Errorf("editing %s must be caught as seal_invalid, got %q ok=%v", field, r, ok)
50 }
51 }
52}
53
54func TestSealFromAnotherKeyIsRefused(t *testing.T) {
55 now := time.Now()
56 c := NewChallenge(ChallengeKey("one"), now, DefaultDifficulty)
57
58 if r, ok := VerifySeal(ChallengeKey("two"), c, now.Add(10*time.Second)); ok {
59 t.Errorf("a seal from a different instance key must not verify, got %q", r)
60 }
61}
62
63// The instance key is shared with rastrillo/auth, which labels its own
64// derivation. Using the raw key here would let a token minted for one
65// subsystem carry weight in the other.
66func TestChallengeKeyIsDerivedNotRaw(t *testing.T) {
67 raw := strings.Repeat("k", 64)
68 if bytes.Equal(ChallengeKey(raw), []byte(raw)) {
69 t.Fatal("the challenge key must be derived from the instance key, not be it")
70 }
71 if bytes.Equal(ChallengeKey("a"), ChallengeKey("b")) {
72 t.Fatal("different instance keys must derive different challenge keys")
73 }
74}
75
76func TestNoncesAreUnique(t *testing.T) {
77 key, now := testKey(), time.Now()
78 seen := map[string]bool{}
79 for i := 0; i < 1000; i++ {
80 n := NewChallenge(key, now, DefaultDifficulty).Nonce
81 if seen[n] {
82 t.Fatalf("nonce %q issued twice", n)
83 }
84 seen[n] = true
85 }
86}
87
88func TestExpiryIsIssueTimePlusMaxAge(t *testing.T) {
89 key, now := testKey(), time.Now()
90 c := NewChallenge(key, now, DefaultDifficulty)
91
92 // A spent nonce only needs remembering while a challenge of its age
93 // would still be accepted; past that, age alone refuses it.
94 want := time.Unix(c.IssuedAt, 0).Add(MaxAge)
95 if !c.Expiry().Equal(want) {
96 t.Fatalf("Expiry = %v, want %v", c.Expiry(), want)
97 }
98}
99