Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

graft-source / internal / activitypub / httpsig_test.go
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}