| 1 | package movement |
| 2 | |
| 3 | import ( |
| 4 | "bytes" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | "time" |
| 8 | ) |
| 9 | |
| 10 | func testKey() []byte { return ChallengeKey(strings.Repeat("k", 64)) } |
| 11 | |
| 12 | func 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 | |
| 21 | func 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. |
| 36 | func 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 | |
| 54 | func 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. |
| 66 | func 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 | |
| 76 | func 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 | |
| 88 | func 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 | |