internal/push/token_test.go
97 lines · 2527 bytes
1package push
2
3import (
4 "crypto/ecdsa"
5 "crypto/elliptic"
6 "crypto/rand"
7 "crypto/sha256"
8 "encoding/base64"
9 "encoding/json"
10 "math/big"
11 "strings"
12 "testing"
13 "time"
14)
15
16func testKey(t *testing.T) *ecdsa.PrivateKey {
17 t.Helper()
18 k, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
19 if err != nil {
20 t.Fatal(err)
21 }
22 return k
23}
24
25func TestTokenShapeAndSignature(t *testing.T) {
26 key := testKey(t)
27 ts := newTokenSource(key, "KEYID123", "TEAMID456")
28 tok, err := ts.token()
29 if err != nil {
30 t.Fatalf("token: %v", err)
31 }
32 parts := strings.Split(tok, ".")
33 if len(parts) != 3 {
34 t.Fatalf("want three dot-separated parts, got %d", len(parts))
35 }
36
37 var hdr struct{ Alg, Kid string }
38 raw, _ := base64.RawURLEncoding.DecodeString(parts[0])
39 if err := json.Unmarshal(raw, &hdr); err != nil {
40 t.Fatalf("header: %v", err)
41 }
42 if hdr.Alg != "ES256" || hdr.Kid != "KEYID123" {
43 t.Fatalf("header = %+v", hdr)
44 }
45
46 // APNs provider tokens carry iss (team id) and iat, and nothing else.
47 var claims map[string]any
48 raw, _ = base64.RawURLEncoding.DecodeString(parts[1])
49 if err := json.Unmarshal(raw, &claims); err != nil {
50 t.Fatalf("claims: %v", err)
51 }
52 if claims["iss"] != "TEAMID456" {
53 t.Fatalf("iss = %v", claims["iss"])
54 }
55 if _, ok := claims["iat"]; !ok {
56 t.Fatal("no iat")
57 }
58 if len(claims) != 2 {
59 t.Fatalf("unexpected claims: %v", claims)
60 }
61
62 // The signature is raw r||s, 64 bytes — not the ASN.1 DER that
63 // ecdsa.SignASN1 returns. Sending DER gets every push rejected.
64 sig, err := base64.RawURLEncoding.DecodeString(parts[2])
65 if err != nil {
66 t.Fatalf("signature not base64url: %v", err)
67 }
68 if len(sig) != 64 {
69 t.Fatalf("signature is %d bytes, want 64 (raw r||s)", len(sig))
70 }
71 sum := sha256.Sum256([]byte(parts[0] + "." + parts[1]))
72 r := new(big.Int).SetBytes(sig[:32])
73 s := new(big.Int).SetBytes(sig[32:])
74 if !ecdsa.Verify(&key.PublicKey, sum[:], r, s) {
75 t.Fatal("signature does not verify")
76 }
77}
78
79func TestTokenCachedThenReminted(t *testing.T) {
80 ts := newTokenSource(testKey(t), "K", "T")
81 base := time.Now()
82 ts.now = func() time.Time { return base }
83
84 first, _ := ts.token()
85 second, _ := ts.token()
86 if first != second {
87 t.Fatal("token reminted inside the cache window; APNs answers TooManyProviderTokenUpdates")
88 }
89
90 // Valid for an hour, not to be reminted faster than every twenty
91 // minutes: refresh at fifty.
92 ts.now = func() time.Time { return base.Add(51 * time.Minute) }
93 third, _ := ts.token()
94 if third == first {
95 t.Fatal("token not reminted after fifty minutes")
96 }
97}