internal/push/token_test.go

419f6dfdc5489a0c6374e36dd1ebbfca68040056
gitbay/internal/push/token_test.go history · blame · raw

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}