Changes
8 changed files (+293/-62)
-
-
@@ -9,9 +9,12 @@ import ("encoding/json" "fmt" "net/http" "net/http/httptest" "net/url" "testing" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/setting" "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams"
-
@@ -32,7 +35,7 @@ func TestActivityPubPerson(t *testing.T) {username := "user2" req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username)) resp := MakeRequest(t, req, http.StatusOK) assert.Contains(t, string(resp.Body.Bytes()), "@context") assert.Contains(t, resp.Body.String(), "@context") var m map[string]interface{} _ = json.Unmarshal(resp.Body.Bytes(), &m)
-
@@ -46,26 +49,26 @@ func TestActivityPubPerson(t *testing.T) {assert.Equal(t, err, nil) assert.Equal(t, "Person", person.GetTypeName()) assert.Equal(t, username, person.GetActivityStreamsName().Begin().GetXMLSchemaString()) keyId := person.GetJSONLDId().GetIRI().String() assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyId) keyID := person.GetJSONLDId().GetIRI().String() assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyID) assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/outbox$", username), person.GetActivityStreamsOutbox().GetIRI().String()) assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.GetActivityStreamsInbox().GetIRI().String()) pkp := person.GetW3IDSecurityV1PublicKey() publicKeyId := keyId + "/#main-key" publicKeyID := keyID + "/#main-key" var pkpFound vocab.W3IDSecurityV1PublicKey for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() { if !pkpIter.IsW3IDSecurityV1PublicKey() { continue } pkValue := pkpIter.Get() var pkId *url.URL pkId, err = pub.GetId(pkValue) var pkID *url.URL pkID, err = pub.GetId(pkValue) if err != nil { return } assert.Equal(t, pkId.String(), publicKeyId) if pkId.String() != publicKeyId { assert.Equal(t, pkID.String(), publicKeyID) if pkID.String() != publicKeyID { continue } pkpFound = pkValue
-
@@ -91,6 +94,40 @@ func TestActivityPubMissingPerson(t *testing.T) {req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser") resp := MakeRequest(t, req, http.StatusNotFound) assert.Contains(t, string(resp.Body.Bytes()), "GetUserByName") assert.Contains(t, resp.Body.String(), "GetUserByName") }) } func TestActivityPubPersonInbox(t *testing.T) { srv := httptest.NewServer(c) defer srv.Close() onGiteaRun(t, func(*testing.T, *url.URL) { appURL := setting.AppURL setting.Federation.Enabled = true setting.Database.LogSQL = true setting.AppURL = srv.URL defer func() { setting.Federation.Enabled = false setting.Database.LogSQL = false setting.AppURL = appURL }() username1 := "user1" user1, err := user_model.GetUserByName(username1) assert.NoError(t, err) user1url := fmt.Sprintf("%s/api/v1/activitypub/user/%s/#main-key", srv.URL, username1) c, err := activitypub.NewClient(user1, user1url) assert.NoError(t, err) username2 := "user2" user2inboxurl := fmt.Sprintf("%s/api/v1/activitypub/user/%s/inbox", srv.URL, username2) // Signed request succeeds resp, err := c.Post([]byte{}, user2inboxurl) assert.NoError(t, err) assert.Equal(t, 204, resp.StatusCode) // Unsigned request fails req := NewRequest(t, "POST", user2inboxurl) MakeRequest(t, req, 500) }) }
-
-
-
@@ -19,10 +19,11 @@ import () const ( activityStreamsContentType = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" // ActivityStreamsContentType const ActivityStreamsContentType = "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"" ) func containsRequiredHttpHeaders(method string, headers []string) error { func containsRequiredHTTPHeaders(method string, headers []string) error { var hasRequestTarget, hasDate, hasDigest bool for _, header := range headers { hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
-
@@ -39,6 +40,7 @@ func containsRequiredHttpHeaders(method string, headers []string) error {return nil } // Client struct type Client struct { clock pub.Clock client *http.Client
-
@@ -47,13 +49,14 @@ type Client struct {getHeaders []string postHeaders []string priv *rsa.PrivateKey pubId string pubID string } func NewClient(user *user_model.User, pubId string) (c *Client, err error) { if err = containsRequiredHttpHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { // NewClient function func NewClient(user *user_model.User, pubID string) (c *Client, err error) { if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil { return } else if err = containsRequiredHttpHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { } else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil { return } else if !httpsig.IsSupportedDigestAlgorithm(setting.Federation.DigestAlgorithm) { err = fmt.Errorf("unsupported digest algorithm: %s", setting.Federation.DigestAlgorithm)
-
@@ -86,21 +89,21 @@ func NewClient(user *user_model.User, pubId string) (c *Client, err error) {getHeaders: setting.Federation.GetHeaders, postHeaders: setting.Federation.PostHeaders, priv: privParsed, pubId: pubId, pubID: pubID, } return } func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { // NewRequest function func (c *Client) NewRequest(b []byte, to string) (req *http.Request, err error) { byteCopy := make([]byte, len(b)) copy(byteCopy, b) buf := bytes.NewBuffer(byteCopy) var req *http.Request req, err = http.NewRequest(http.MethodPost, to, buf) if err != nil { return } req.Header.Add("Content-Type", activityStreamsContentType) req.Header.Add("Content-Type", ActivityStreamsContentType) req.Header.Add("Accept-Charset", "utf-8") req.Header.Add("Date", fmt.Sprintf("%s GMT", c.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")))
-
@@ -108,8 +111,14 @@ func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {if err != nil { return } err = signer.SignRequest(c.priv, c.pubId, req, b) if err != nil { err = signer.SignRequest(c.priv, c.pubID, req, b) return } // Post function func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) { var req *http.Request if req, err = c.NewRequest(b, to); err != nil { return } resp, err = c.client.Do(req)
-
-
-
@@ -7,7 +7,6 @@ package activitypubimport ( "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "regexp"
-
@@ -24,16 +23,16 @@ import (func TestActivityPubSignedPost(t *testing.T) { assert.NoError(t, unittest.PrepareTestDatabase()) user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User) pubId := "https://example.com/pubId" c, err := NewClient(user, pubId) pubID := "https://example.com/pubID" c, err := NewClient(user, pubID) assert.NoError(t, err) expected := "BODY" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest")) assert.Contains(t, r.Header.Get("Signature"), pubId) assert.Equal(t, r.Header.Get("Content-Type"), activityStreamsContentType) body, err := ioutil.ReadAll(r.Body) assert.Contains(t, r.Header.Get("Signature"), pubID) assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType) body, err := io.ReadAll(r.Body) assert.NoError(t, err) assert.Equal(t, expected, string(body)) fmt.Fprintf(w, expected)
-
-
-
@@ -4,6 +4,7 @@package structs // ActivityPub type type ActivityPub struct { Context string `json:"@context"` }
-
-
-
@@ -9,7 +9,6 @@ import ("net/url" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/activitypub" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting"
-
@@ -17,32 +16,9 @@ import ("github.com/go-fed/activity/streams" ) // hack waiting on https://github.com/go-gitea/gitea/pull/16834 func GetPublicKey(user *models.User) (string, error) { if settings, err := models.GetUserSetting(user.ID, []string{"activitypub_pubPem"}); err != nil { return "", err } else if len(settings) == 0 { if priv, pub, err := activitypub.GenerateKeyPair(); err != nil { return "", err } else { privPem := &models.UserSetting{UserID: user.ID, Name: "activitypub_privPem", Value: priv} if err := models.SetUserSetting(privPem); err != nil { return "", err } pubPem := &models.UserSetting{UserID: user.ID, Name: "activitypub_pubPem", Value: pub} if err := models.SetUserSetting(pubPem); err != nil { return "", err } return pubPem.Value, nil } } else { return settings[0].Value, nil } } // NodeInfo returns the NodeInfo for the Gitea instance to allow for federation // Person function func Person(ctx *context.APIContext) { // swagger:operation GET /activitypub/user/{username} information // swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson // --- // summary: Returns the person // produces:
-
@@ -73,30 +49,30 @@ func Person(ctx *context.APIContext) {person.SetActivityStreamsName(name) ibox := streams.NewActivityStreamsInboxProperty() url_object, _ := url.Parse(link + "/inbox") ibox.SetIRI(url_object) urlObject, _ := url.Parse(link + "/inbox") ibox.SetIRI(urlObject) person.SetActivityStreamsInbox(ibox) obox := streams.NewActivityStreamsOutboxProperty() url_object, _ = url.Parse(link + "/outbox") obox.SetIRI(url_object) urlObject, _ = url.Parse(link + "/outbox") obox.SetIRI(urlObject) person.SetActivityStreamsOutbox(obox) publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() publicKeyType := streams.NewW3IDSecurityV1PublicKey() pubKeyIdProp := streams.NewJSONLDIdProperty() pubKeyIDProp := streams.NewJSONLDIdProperty() pubKeyIRI, _ := url.Parse(link + "/#main-key") pubKeyIdProp.SetIRI(pubKeyIRI) publicKeyType.SetJSONLDId(pubKeyIdProp) pubKeyIDProp.SetIRI(pubKeyIRI) publicKeyType.SetJSONLDId(pubKeyIDProp) ownerProp := streams.NewW3IDSecurityV1OwnerProperty() ownerProp.SetIRI(idIRI) publicKeyType.SetW3IDSecurityV1Owner(ownerProp) publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() if publicKeyPem, err := GetPublicKey(user); err != nil { if publicKeyPem, err := activitypub.GetPublicKey(user); err != nil { ctx.Error(http.StatusInternalServerError, "GetPublicKey", err) } else { publicKeyPemProp.Set(publicKeyPem)
-
@@ -110,3 +86,24 @@ func Person(ctx *context.APIContext) {jsonmap, _ = streams.Serialize(person) ctx.JSON(http.StatusOK, jsonmap) } // PersonInbox function func PersonInbox(ctx *context.APIContext) { // swagger:operation POST /activitypub/user/{username}/inbox activitypub activitypubPersonInbox // --- // summary: Send to the inbox // produces: // - application/json // parameters: // - name: username // in: path // description: username of the user // type: string // required: true // responses: // responses: // "204": // "$ref": "#/responses/empty" ctx.Status(http.StatusNoContent) }
-
-
-
@@ -0,0 +1,158 @@// Copyright 2021 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package activitypub import ( "context" "crypto" "crypto/x509" "encoding/pem" "fmt" "io" "net/http" "net/url" "code.gitea.io/gitea/modules/activitypub" gitea_context "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/setting" "github.com/go-fed/activity/pub" "github.com/go-fed/activity/streams" "github.com/go-fed/activity/streams/vocab" "github.com/go-fed/httpsig" ) type publicKeyer interface { GetW3IDSecurityV1PublicKey() vocab.W3IDSecurityV1PublicKeyProperty } func getPublicKeyFromResponse(ctx context.Context, b []byte, keyID *url.URL) (p crypto.PublicKey, err error) { m := make(map[string]interface{}) err = json.Unmarshal(b, &m) if err != nil { return } var t vocab.Type t, err = streams.ToType(ctx, m) if err != nil { return } pker, ok := t.(publicKeyer) if !ok { err = fmt.Errorf("ActivityStreams type cannot be converted to one known to have publicKey property: %T", t) return } pkp := pker.GetW3IDSecurityV1PublicKey() if pkp == nil { err = fmt.Errorf("publicKey property is not provided") return } var pkpFound vocab.W3IDSecurityV1PublicKey for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() { if !pkpIter.IsW3IDSecurityV1PublicKey() { continue } pkValue := pkpIter.Get() var pkID *url.URL pkID, err = pub.GetId(pkValue) if err != nil { return } if pkID.String() != keyID.String() { continue } pkpFound = pkValue break } if pkpFound == nil { err = fmt.Errorf("cannot find publicKey with id: %s in %s", keyID, b) return } pkPemProp := pkpFound.GetW3IDSecurityV1PublicKeyPem() if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { err = fmt.Errorf("publicKeyPem property is not provided or it is not embedded as a value") return } pubKeyPem := pkPemProp.Get() var block *pem.Block block, _ = pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { err = fmt.Errorf("could not decode publicKeyPem to PUBLIC KEY pem block type") return } p, err = x509.ParsePKIXPublicKey(block.Bytes) return } func fetch(iri *url.URL) (b []byte, err error) { var req *http.Request req, err = http.NewRequest(http.MethodGet, iri.String(), nil) if err != nil { return } req.Header.Add("Accept", activitypub.ActivityStreamsContentType) req.Header.Add("Accept-Charset", "utf-8") clock, err := activitypub.NewClock() if err != nil { return } req.Header.Add("Date", fmt.Sprintf("%s GMT", clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05"))) var resp *http.Response client := &http.Client{} resp, err = client.Do(req) if err != nil { return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { err = fmt.Errorf("url IRI fetch [%s] failed with status (%d): %s", iri, resp.StatusCode, resp.Status) return } b, err = io.ReadAll(resp.Body) return } func verifyHTTPSignatures(ctx *gitea_context.APIContext) (authenticated bool, err error) { r := ctx.Req // 1. Figure out what key we need to verify var v httpsig.Verifier v, err = httpsig.NewVerifier(r) if err != nil { return } ID := v.KeyId() var idIRI *url.URL idIRI, err = url.Parse(ID) if err != nil { return } // 2. Fetch the public key of the other actor var b []byte b, err = fetch(idIRI) if err != nil { return } pKey, err := getPublicKeyFromResponse(*ctx, b, idIRI) if err != nil { return } // 3. Verify the other actor's key algo := httpsig.Algorithm(setting.Federation.Algorithms[0]) authenticated = nil == v.Verify(pKey, algo) return } // ReqSignature function func ReqSignature() func(ctx *gitea_context.APIContext) { return func(ctx *gitea_context.APIContext) { if authenticated, err := verifyHTTPSignatures(ctx); err != nil { ctx.Error(http.StatusInternalServerError, "verifyHttpSignatures", err) } else if !authenticated { ctx.Error(http.StatusForbidden, "reqSignature", "request signature verification failed") } } }
-
-
-
@@ -602,6 +602,7 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {m.Group("/user/{username}", func() { m.Get("", activitypub.Person) }) m.Post("/user/{username}/inbox", activitypub.ReqSignature(), activitypub.PersonInbox) }) } m.Get("/signing-key.gpg", misc.SigningKey)
-
-
-
@@ -28,8 +28,11 @@"produces": [ "application/json" ], "tags": [ "activitypub" ], "summary": "Returns the person", "operationId": "information", "operationId": "activitypubPerson", "parameters": [ { "type": "string",
-
@@ -46,6 +49,32 @@} } }, "/activitypub/user/{username}/inbox": { "post": { "produces": [ "application/json" ], "tags": [ "activitypub" ], "summary": "Send to the inbox", "operationId": "activitypubPersonInbox", "parameters": [ { "type": "string", "description": "username of the user", "name": "username", "in": "path", "required": true } ], "responses": { "204": { "$ref": "#/responses/empty" } } } }, "/admin/cron": { "get": { "produces": [
-