diff --git a/pub/fed_test.go b/pub/fed_test.go index 5512dcf..bd72e1e 100644 --- a/pub/fed_test.go +++ b/pub/fed_test.go @@ -3,10 +3,14 @@ package pub import ( "bytes" "context" + "crypto" + "crypto/rand" + "crypto/rsa" "encoding/json" "fmt" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/vocab" + "github.com/go-fed/httpsig" "io/ioutil" "net/http" "net/http/httptest" @@ -34,6 +38,8 @@ const ( noteName = "A Note" otherOriginIRIString = "https://foo.net/activity/112358" otherOriginActorIRIString = "https://foo.net/peyton" + + testPublicKeyID = "publicKeyId" ) var ( @@ -85,6 +91,8 @@ var ( testClientExpectedLike *vocab.Like testClientExpectedUndo *vocab.Undo testClientExpectedBlock *vocab.Block + + testPrivateKey *rsa.PrivateKey ) func init() { @@ -391,6 +399,11 @@ func init() { testClientExpectedBlock.SetId(*testNewIRI) testClientExpectedBlock.AddActorObject(sallyActor) testClientExpectedBlock.AddObject(samActor) + + testPrivateKey, err = rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + panic(err) + } } func Must(l *time.Location, e error) *time.Location { @@ -734,6 +747,8 @@ type MockFederateApp struct { unblocked func(c context.Context, actorIRIs []url.URL) error getFollowing func(c context.Context, actor url.URL) (vocab.CollectionType, error) filterForwarding func(c context.Context, activity vocab.ActivityType, iris []url.URL) ([]url.URL, error) + newSigner func() (httpsig.Signer, error) + privateKey func(boxIRI url.URL) (crypto.PrivateKey, string, error) } func (m *MockFederateApp) CanAdd(c context.Context, obj vocab.ObjectType, target vocab.ObjectType) bool { @@ -778,6 +793,20 @@ func (m *MockFederateApp) FilterForwarding(c context.Context, activity vocab.Act return m.filterForwarding(c, activity, iris) } +func (m *MockFederateApp) NewSigner() (httpsig.Signer, error) { + if m.newSigner == nil { + m.t.Fatal("unexpected call to MockFederateApp NewSigner") + } + return m.newSigner() +} + +func (m *MockFederateApp) PrivateKey(boxIRI url.URL) (privKey crypto.PrivateKey, pubKeyId string, err error) { + if m.privateKey == nil { + m.t.Fatal("unexpected call to MockFederateApp PrivateKey") + } + return m.privateKey(boxIRI) +} + var _ Deliverer = &MockDeliverer{} type MockDeliverer struct { @@ -861,6 +890,16 @@ func PreparePostOutboxTest(t *testing.T, app *MockApplication, socialApp *MockSo socialApp.postOutboxAuthorized = func(c context.Context, r *http.Request) (bool, error) { return true, nil } + fedApp.newSigner = func() (httpsig.Signer, error) { + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + return testPrivateKey, testPublicKeyID, nil + } app.newId = func(c context.Context, t Typer) url.URL { return *testNewIRI } @@ -1324,7 +1363,7 @@ func TestPubber_GetInbox(t *testing.T) { } func TestPubber_PostOutbox(t *testing.T) { - app, socialApp, _, socialCb, _, d, httpClient, p := NewPubberTest(t) + app, socialApp, fedApp, socialCb, _, d, httpClient, p := NewPubberTest(t) resp := httptest.NewRecorder() req := ActivityPubRequest(httptest.NewRequest("POST", testOutboxURI, bytes.NewBuffer(MustSerialize(testCreateNote)))) gotPostOutboxAuthorized := 0 @@ -1332,6 +1371,22 @@ func TestPubber_PostOutbox(t *testing.T) { gotPostOutboxAuthorized++ return true, nil } + gotNewSigner := 0 + fedApp.newSigner = func() (httpsig.Signer, error) { + gotNewSigner++ + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + gotPrivateKey := 0 + var gotPrivateKeyIRI url.URL + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + gotPrivateKey++ + gotPrivateKeyIRI = boxIRI + return testPrivateKey, testPublicKeyID, nil + } gotNewId := 0 app.newId = func(c context.Context, t Typer) url.URL { gotNewId++ @@ -1411,6 +1466,12 @@ func TestPubber_PostOutbox(t *testing.T) { t.Fatalf("expected %d, got %d", 1, gotPostOutboxAuthorized) } else if gotNewId != 1 { t.Fatalf("expected %d, got %d", 1, gotNewId) + } else if gotNewSigner != 1 { + t.Fatalf("expected %d, got %d", 1, gotNewSigner) + } else if gotPrivateKey != 1 { + t.Fatalf("expected %d, got %d", 1, gotPrivateKey) + } else if s := (&gotPrivateKeyIRI).String(); s != testOutboxURI { + t.Fatalf("expected %s, got %s", testOutboxURI, s) } else if gotOutbox != 1 { t.Fatalf("expected %d, got %d", 1, gotOutbox) } else if gotSet != 2 { @@ -1454,6 +1515,14 @@ func TestPubber_PostOutbox(t *testing.T) { } else if resp.Code != http.StatusCreated { t.Fatalf("expected %d, got %d", http.StatusCreated, resp.Code) } + verif, err := httpsig.NewVerifier(httpDeliveryRequest) + if err != nil { + t.Fatal(err) + } else if verif.KeyId() != testPublicKeyID { + t.Fatalf("expected %s, got %s", testPublicKeyID, verif.KeyId()) + } else if err := verif.Verify(testPrivateKey.Public(), httpsig.RSA_SHA256); err != nil { + t.Fatal(err) + } } func TestPubber_GetOutbox(t *testing.T) { @@ -2094,6 +2163,22 @@ func TestPostInbox_Follow_AutoReject(t *testing.T) { gotOnFollow++ return AutomaticReject } + gotNewSigner := 0 + fedApp.newSigner = func() (httpsig.Signer, error) { + gotNewSigner++ + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + gotPrivateKey := 0 + var gotPrivateKeyIRI url.URL + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + gotPrivateKey++ + gotPrivateKeyIRI = boxIRI + return testPrivateKey, testPublicKeyID, nil + } fedCb.follow = func(c context.Context, s *streams.Follow) error { return nil } @@ -2145,6 +2230,12 @@ func TestPostInbox_Follow_AutoReject(t *testing.T) { t.Fatal(err) } else if !handled { t.Fatalf("expected handled, got !handled") + } else if gotNewSigner != 1 { + t.Fatalf("expected %d, got %d", 1, gotNewSigner) + } else if gotPrivateKey != 1 { + t.Fatalf("expected %d, got %d", 1, gotPrivateKey) + } else if s := (&gotPrivateKeyIRI).String(); s != testInboxURI { + t.Fatalf("expected %s, got %s", testInboxURI, s) } else if gotOnFollow != 1 { t.Fatalf("expected %d, got %d", 1, gotOnFollow) } else if gotHttpDo != 2 { @@ -2176,6 +2267,22 @@ func TestPostInbox_Follow_AutoAccept(t *testing.T) { gotOnFollow++ return AutomaticAccept } + gotNewSigner := 0 + fedApp.newSigner = func() (httpsig.Signer, error) { + gotNewSigner++ + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + gotPrivateKey := 0 + var gotPrivateKeyIRI url.URL + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + gotPrivateKey++ + gotPrivateKeyIRI = boxIRI + return testPrivateKey, testPublicKeyID, nil + } fedCb.follow = func(c context.Context, s *streams.Follow) error { return nil } @@ -2255,6 +2362,12 @@ func TestPostInbox_Follow_AutoAccept(t *testing.T) { t.Fatalf("expected handled, got !handled") } else if gotOnFollow != 1 { t.Fatalf("expected %d, got %d", 1, gotOnFollow) + } else if gotNewSigner != 1 { + t.Fatalf("expected %d, got %d", 1, gotNewSigner) + } else if gotPrivateKey != 1 { + t.Fatalf("expected %d, got %d", 1, gotPrivateKey) + } else if s := (&gotPrivateKeyIRI).String(); s != testInboxURI { + t.Fatalf("expected %s, got %s", testInboxURI, s) } else if gotHttpDo != 2 { t.Fatalf("expected %d, got %d", 2, gotHttpDo) } else if s := httpActorRequest.URL.String(); s != sallyIRIString { @@ -2290,6 +2403,16 @@ func TestPostInbox_Follow_DoesNotAddForAutoAcceptIfAlreadyPresent(t *testing.T) fedApp.onFollow = func(c context.Context, s *streams.Follow) FollowResponse { return AutomaticAccept } + fedApp.newSigner = func() (httpsig.Signer, error) { + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + return testPrivateKey, testPublicKeyID, nil + } fedCb.follow = func(c context.Context, s *streams.Follow) error { return nil } @@ -2363,6 +2486,16 @@ func TestPostInbox_Follow_AutoAcceptFollowersIsOrderedCollection(t *testing.T) { fedApp.onFollow = func(c context.Context, s *streams.Follow) FollowResponse { return AutomaticAccept } + fedApp.newSigner = func() (httpsig.Signer, error) { + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + return testPrivateKey, testPublicKeyID, nil + } fedCb.follow = func(c context.Context, s *streams.Follow) error { return nil } @@ -2432,6 +2565,16 @@ func TestPostInbox_Follow_AutoAcceptFollowersIsIRI(t *testing.T) { fedApp.onFollow = func(c context.Context, s *streams.Follow) FollowResponse { return AutomaticAccept } + fedApp.newSigner = func() (httpsig.Signer, error) { + s, _, err := httpsig.NewSigner([]httpsig.Algorithm{httpsig.RSA_SHA256}, nil, httpsig.Signature) + if err != nil { + t.Fatal(err) + } + return s, err + } + fedApp.privateKey = func(boxIRI url.URL) (crypto.PrivateKey, string, error) { + return testPrivateKey, testPublicKeyID, nil + } fedCb.follow = func(c context.Context, s *streams.Follow) error { return nil } diff --git a/pub/internal.go b/pub/internal.go index 050c924..af1d90d 100644 --- a/pub/internal.go +++ b/pub/internal.go @@ -73,7 +73,7 @@ func addJSONLDContext(m map[string]interface{}) { // ActivityStream representation. // // creds is allowed to be nil. -func dereference(c HttpClient, u url.URL, agent string, creds *creds) ([]byte, error) { +func dereference(c HttpClient, u url.URL, agent string, creds *creds, clock Clock) ([]byte, error) { // TODO: (section 7.1) The server MUST dereference the collection (with the user's credentials) req, err := http.NewRequest("GET", u.String(), nil) if err != nil { @@ -81,7 +81,7 @@ func dereference(c HttpClient, u url.URL, agent string, creds *creds) ([]byte, e } req.Header.Add(acceptHeader, getAcceptHeader) req.Header.Add("Accept-Charset", "utf-8") - req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + req.Header.Add("Date", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") req.Header.Add("User-Agent", fmt.Sprintf("%s (go-fed ActivityPub)", agent)) if creds != nil { err := creds.signer.SignRequest(creds.privKey, creds.pubKeyId, req) @@ -111,7 +111,7 @@ type creds struct { // body set to the provided bytes. // // creds is able to be nil. -func postToOutbox(c HttpClient, b []byte, to url.URL, agent string, creds *creds) error { +func postToOutbox(c HttpClient, b []byte, to url.URL, agent string, creds *creds, clock Clock) error { byteCopy := make([]byte, 0, len(b)) copy(b, byteCopy) buf := bytes.NewBuffer(byteCopy) @@ -121,7 +121,7 @@ func postToOutbox(c HttpClient, b []byte, to url.URL, agent string, creds *creds } req.Header.Add(contentTypeHeader, postContentTypeHeader) req.Header.Add("Accept-Charset", "utf-8") - req.Header.Add("Date", time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") + req.Header.Add("Date", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT") req.Header.Add("User-Agent", fmt.Sprintf("%s (go-fed ActivityPub)", agent)) if creds != nil { err := creds.signer.SignRequest(creds.privKey, creds.pubKeyId, req) @@ -509,7 +509,7 @@ func (f *federator) deliver(obj vocab.ActivityType, boxIRI url.URL) error { if err != nil { return err } - var creds *creds + creds := &creds{} creds.signer, err = f.FederateApp.NewSigner() if err != nil { return err @@ -535,7 +535,7 @@ func (f *federator) deliverToRecipients(obj vocab.ActivityType, recipients []url } for _, to := range recipients { f.deliverer.Do(b, to, func(b []byte, u url.URL) error { - return postToOutbox(f.Client, b, u, f.Agent, creds) + return postToOutbox(f.Client, b, u, f.Agent, creds, f.Clock) }) } return nil @@ -620,7 +620,7 @@ func (c *federator) resolveInboxes(boxIRI url.URL, r []url.URL, depth int, max i uris = getURIsInItemer(c) return nil } - err := doForCollectionPage(c.Client, c.Agent, cb, cp.Raw(), cr) + err := doForCollectionPage(c.Client, c.Agent, cb, cp.Raw(), cr, c.Clock) if err != nil { return nil, err } @@ -634,7 +634,7 @@ func (c *federator) resolveInboxes(boxIRI url.URL, r []url.URL, depth int, max i uris = getURIsInOrderedItemer(c) return nil } - err := doForOrderedCollectionPage(c.Client, c.Agent, cb, ocp.Raw(), cr) + err := doForOrderedCollectionPage(c.Client, c.Agent, cb, ocp.Raw(), cr, c.Clock) if err != nil { return nil, err } @@ -654,7 +654,7 @@ func (c *federator) dereferenceForResolvingInboxes(u, boxIRI url.URL, cr *creds) // To pass back to calling function, since may be set recursively: cred = cr var resp []byte - resp, err = dereference(c.Client, u, c.Agent, cr) + resp, err = dereference(c.Client, u, c.Agent, cr, c.Clock) if err != nil { return } @@ -806,7 +806,7 @@ func (f *federator) dedupeOrderedItems(oc vocab.OrderedCollectionType) (vocab.Or id = pIri.String() } else if oc.IsOrderedItemsIRI(i) { removeFn = oc.RemoveOrderedItemsIRI - b, err := dereference(f.Client, oc.GetOrderedItemsIRI(i), f.Agent, nil) + b, err := dereference(f.Client, oc.GetOrderedItemsIRI(i), f.Agent, nil, f.Clock) var m map[string]interface{} if err := json.Unmarshal(b, &m); err != nil { return oc, err @@ -944,7 +944,7 @@ func getAudienceIRIs(o deliverableObject) []url.URL { // doForCollectionPage applies a function over a collection and its subsequent // pages recursively. It returns the first non-nil error it encounters. -func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionPageType) error, c vocab.CollectionPageType, creds *creds) error { +func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionPageType) error, c vocab.CollectionPageType, creds *creds, clock Clock) error { err := cb(c) if err != nil { return err @@ -952,12 +952,12 @@ func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionP if c.IsNextCollectionPage() { // Handle this one weird trick that other peers HATE federating // with. - return doForCollectionPage(h, agent, cb, c.GetNextCollectionPage(), creds) + return doForCollectionPage(h, agent, cb, c.GetNextCollectionPage(), creds, clock) } else if c.IsNextLink() { l := c.GetNextLink() if l.HasHref() { u := l.GetHref() - resp, err := dereference(h, u, agent, creds) + resp, err := dereference(h, u, agent, creds, clock) if err != nil { return err } @@ -971,12 +971,12 @@ func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionP return err } if next != nil { - return doForCollectionPage(h, agent, cb, next.Raw(), creds) + return doForCollectionPage(h, agent, cb, next.Raw(), creds, clock) } } } else if c.IsNextIRI() { u := c.GetNextIRI() - resp, err := dereference(h, u, agent, creds) + resp, err := dereference(h, u, agent, creds, clock) if err != nil { return err } @@ -990,7 +990,7 @@ func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionP return err } if next != nil { - return doForCollectionPage(h, agent, cb, next.Raw(), creds) + return doForCollectionPage(h, agent, cb, next.Raw(), creds, clock) } } return nil @@ -999,7 +999,7 @@ func doForCollectionPage(h HttpClient, agent string, cb func(c vocab.CollectionP // doForOrderedCollectionPage applies a function over a collection and its // subsequent pages recursively. It returns the first non-nil error it // encounters. -func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.OrderedCollectionPageType) error, c vocab.OrderedCollectionPageType, creds *creds) error { +func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.OrderedCollectionPageType) error, c vocab.OrderedCollectionPageType, creds *creds, clock Clock) error { err := cb(c) if err != nil { return err @@ -1007,12 +1007,12 @@ func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.Orde if c.IsNextOrderedCollectionPage() { // Handle this one weird trick that other peers HATE federating // with. - return doForOrderedCollectionPage(h, agent, cb, c.GetNextOrderedCollectionPage(), creds) + return doForOrderedCollectionPage(h, agent, cb, c.GetNextOrderedCollectionPage(), creds, clock) } else if c.IsNextLink() { l := c.GetNextLink() if l.HasHref() { u := l.GetHref() - resp, err := dereference(h, u, agent, creds) + resp, err := dereference(h, u, agent, creds, clock) if err != nil { return err } @@ -1026,12 +1026,12 @@ func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.Orde return err } if next != nil { - return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds) + return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds, clock) } } } else if c.IsNextIRI() { u := c.GetNextIRI() - resp, err := dereference(h, u, agent, creds) + resp, err := dereference(h, u, agent, creds, clock) if err != nil { return err } @@ -1045,7 +1045,7 @@ func doForOrderedCollectionPage(h HttpClient, agent string, cb func(c vocab.Orde return err } if next != nil { - return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds) + return doForOrderedCollectionPage(h, agent, cb, next.Raw(), creds, clock) } } return nil