Federated via graft
4.7 kB
128 lines
1package activitypub
2
3import (
4 "bytes"
5 "crypto"
6 "crypto/rand"
7 "crypto/rsa"
8 "crypto/sha256"
9 "encoding/base64"
10 "fmt"
11 "io"
12 "net/http"
13 "net/http/httptest"
14 "strings"
15 "testing"
16 "time"
17)
18
19// TestSignVerifyRoundTrip exercises the actual code path a remote server
20// runs: sign a POST the way PostSigned does, receive it through an
21// httptest server, and verify it the way the inbox handler does. This is
22// the one piece of this package genuinely worth distrusting until proven —
23// HTTP Signatures are notoriously easy to get subtly wrong (header casing,
24// request-target format, digest encoding) in a way that only shows up
25// against a real remote server's strict verifier.
26func TestSignVerifyRoundTrip(t *testing.T) {
27 _, pub, priv := testKeyPair(t)
28
29 body := []byte(`{"type":"Follow","actor":"https://example.social/users/alice","object":"https://graft.example.org/actors/testrepo"}`)
30
31 var verifyErr error
32 var gotBody []byte
33 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
34 var err error
35 gotBody, err = io.ReadAll(r.Body)
36 if err != nil {
37 t.Errorf("read body: %v", err)
38 }
39 verifyErr = VerifyRequest(r, gotBody, pub)
40 w.WriteHeader(http.StatusAccepted)
41 }))
42 defer srv.Close()
43
44 client := srv.Client()
45 if err := PostSigned(client, srv.URL+"/actors/testrepo/inbox", "https://example.social/users/alice#main-key", priv, body); err != nil {
46 t.Fatalf("PostSigned: %v", err)
47 }
48 if verifyErr != nil {
49 t.Fatalf("VerifyRequest: %v", verifyErr)
50 }
51 if !bytes.Equal(gotBody, body) {
52 t.Fatalf("body mismatch: got %q want %q", gotBody, body)
53 }
54}
55
56// TestVerifyRejectsTamperedBody confirms a body modified in transit (or by
57// a malicious intermediary) fails verification via the Digest mismatch,
58// even though the Signature header itself is untouched.
59func TestVerifyRejectsTamperedBody(t *testing.T) {
60 _, pub, priv := testKeyPair(t)
61 body := []byte(`{"type":"Follow","actor":"https://example.social/users/alice"}`)
62
63 req, err := http.NewRequest(http.MethodPost, "https://graft.example.org/actors/testrepo/inbox", bytes.NewReader(body))
64 if err != nil {
65 t.Fatal(err)
66 }
67 if err := SignRequest(req, body, "https://example.social/users/alice#main-key", priv); err != nil {
68 t.Fatal(err)
69 }
70
71 tampered := []byte(`{"type":"Follow","actor":"https://attacker.example/users/mallory"}`)
72 if err := VerifyRequest(req, tampered, pub); err == nil {
73 t.Fatal("expected verification to fail on tampered body, got nil error")
74 }
75}
76
77// TestVerifyRejectsStaleDate confirms a byte-for-byte replay of an old,
78// genuinely-validly-signed request is rejected: the signature itself
79// verifies fine (Date is part of what was signed, and hasn't changed —
80// this is a faithful replay, not tampering), but the freshness check must
81// still catch that "now" is far past the signed Date. SignRequest always
82// stamps the current time, so this builds the signature by hand with a
83// fixed old Date to simulate a captured-and-replayed request correctly.
84func TestVerifyRejectsStaleDate(t *testing.T) {
85 _, pub, priv := testKeyPair(t)
86 body := []byte(`{"type":"Follow","actor":"https://example.social/users/alice"}`)
87 keyID := "https://example.social/users/alice#main-key"
88
89 req, err := http.NewRequest(http.MethodPost, "https://graft.example.org/actors/testrepo/inbox", bytes.NewReader(body))
90 if err != nil {
91 t.Fatal(err)
92 }
93 oldDate := time.Now().Add(-1 * time.Hour).UTC().Format(http.TimeFormat)
94 digest := sha256.Sum256(body)
95 req.Header.Set("Digest", "SHA-256="+base64.StdEncoding.EncodeToString(digest[:]))
96 req.Header.Set("Date", oldDate)
97
98 signingString := buildSigningString(signedHeaders, req.Method, req.URL.RequestURI(), req.URL.Host, req.Header)
99 hashed := sha256.Sum256([]byte(signingString))
100 sig, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, hashed[:])
101 if err != nil {
102 t.Fatal(err)
103 }
104 req.Header.Set("Signature", fmt.Sprintf(
105 `keyId="%s",algorithm="rsa-sha256",headers="%s",signature="%s"`,
106 keyID, strings.Join(signedHeaders, " "), base64.StdEncoding.EncodeToString(sig)))
107
108 if err := VerifyRequest(req, body, pub); err == nil {
109 t.Fatal("expected verification to fail on a stale/replayed Date header, got nil error")
110 }
111}
112
113func testKeyPair(t *testing.T) (privPEM string, pub *rsa.PublicKey, priv *rsa.PrivateKey) {
114 t.Helper()
115 privateP, publicP, err := GenerateKeyPair()
116 if err != nil {
117 t.Fatalf("GenerateKeyPair: %v", err)
118 }
119 privKey, err := ParsePrivateKey(privateP)
120 if err != nil {
121 t.Fatalf("ParsePrivateKey: %v", err)
122 }
123 pubKey, err := ParsePublicKey(publicP)
124 if err != nil {
125 t.Fatalf("ParsePublicKey: %v", err)
126 }
127 return privateP, pubKey, privKey
128}