mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 13:28:25 +00:00 
			
		
		
		
	activitypub: implement the ReqSignature middleware
Signed-off-by: Loïc Dachary <loic@dachary.org>
This commit is contained in:
		
				
					committed by
					
						
						Anthony Wang
					
				
			
			
				
	
			
			
			
						parent
						
							15c1f6218c
						
					
				
				
					commit
					97fedf2616
				
			@@ -9,9 +9,12 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/http/httptest"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						"code.gitea.io/gitea/modules/activitypub"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"github.com/go-fed/activity/pub"
 | 
						"github.com/go-fed/activity/pub"
 | 
				
			||||||
	"github.com/go-fed/activity/streams"
 | 
						"github.com/go-fed/activity/streams"
 | 
				
			||||||
@@ -32,7 +35,7 @@ func TestActivityPubPerson(t *testing.T) {
 | 
				
			|||||||
		username := "user2"
 | 
							username := "user2"
 | 
				
			||||||
		req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username))
 | 
							req := NewRequestf(t, "GET", fmt.Sprintf("/api/v1/activitypub/user/%s", username))
 | 
				
			||||||
		resp := MakeRequest(t, req, http.StatusOK)
 | 
							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{}
 | 
							var m map[string]interface{}
 | 
				
			||||||
		_ = json.Unmarshal(resp.Body.Bytes(), &m)
 | 
							_ = json.Unmarshal(resp.Body.Bytes(), &m)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,26 +49,26 @@ func TestActivityPubPerson(t *testing.T) {
 | 
				
			|||||||
		assert.Equal(t, err, nil)
 | 
							assert.Equal(t, err, nil)
 | 
				
			||||||
		assert.Equal(t, "Person", person.GetTypeName())
 | 
							assert.Equal(t, "Person", person.GetTypeName())
 | 
				
			||||||
		assert.Equal(t, username, person.GetActivityStreamsName().Begin().GetXMLSchemaString())
 | 
							assert.Equal(t, username, person.GetActivityStreamsName().Begin().GetXMLSchemaString())
 | 
				
			||||||
		keyId := person.GetJSONLDId().GetIRI().String()
 | 
							keyID := person.GetJSONLDId().GetIRI().String()
 | 
				
			||||||
		assert.Regexp(t, fmt.Sprintf("activitypub/user/%s$", username), keyId)
 | 
							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/outbox$", username), person.GetActivityStreamsOutbox().GetIRI().String())
 | 
				
			||||||
		assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.GetActivityStreamsInbox().GetIRI().String())
 | 
							assert.Regexp(t, fmt.Sprintf("activitypub/user/%s/inbox$", username), person.GetActivityStreamsInbox().GetIRI().String())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		pkp := person.GetW3IDSecurityV1PublicKey()
 | 
							pkp := person.GetW3IDSecurityV1PublicKey()
 | 
				
			||||||
		publicKeyId := keyId + "/#main-key"
 | 
							publicKeyID := keyID + "/#main-key"
 | 
				
			||||||
		var pkpFound vocab.W3IDSecurityV1PublicKey
 | 
							var pkpFound vocab.W3IDSecurityV1PublicKey
 | 
				
			||||||
		for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
 | 
							for pkpIter := pkp.Begin(); pkpIter != pkp.End(); pkpIter = pkpIter.Next() {
 | 
				
			||||||
			if !pkpIter.IsW3IDSecurityV1PublicKey() {
 | 
								if !pkpIter.IsW3IDSecurityV1PublicKey() {
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			pkValue := pkpIter.Get()
 | 
								pkValue := pkpIter.Get()
 | 
				
			||||||
			var pkId *url.URL
 | 
								var pkID *url.URL
 | 
				
			||||||
			pkId, err = pub.GetId(pkValue)
 | 
								pkID, err = pub.GetId(pkValue)
 | 
				
			||||||
			if err != nil {
 | 
								if err != nil {
 | 
				
			||||||
				return
 | 
									return
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			assert.Equal(t, pkId.String(), publicKeyId)
 | 
								assert.Equal(t, pkID.String(), publicKeyID)
 | 
				
			||||||
			if pkId.String() != publicKeyId {
 | 
								if pkID.String() != publicKeyID {
 | 
				
			||||||
				continue
 | 
									continue
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			pkpFound = pkValue
 | 
								pkpFound = pkValue
 | 
				
			||||||
@@ -91,6 +94,40 @@ func TestActivityPubMissingPerson(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser")
 | 
							req := NewRequestf(t, "GET", "/api/v1/activitypub/user/nonexistentuser")
 | 
				
			||||||
		resp := MakeRequest(t, req, http.StatusNotFound)
 | 
							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 (
 | 
					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
 | 
						var hasRequestTarget, hasDate, hasDigest bool
 | 
				
			||||||
	for _, header := range headers {
 | 
						for _, header := range headers {
 | 
				
			||||||
		hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
 | 
							hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
 | 
				
			||||||
@@ -39,6 +40,7 @@ func containsRequiredHttpHeaders(method string, headers []string) error {
 | 
				
			|||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Client struct
 | 
				
			||||||
type Client struct {
 | 
					type Client struct {
 | 
				
			||||||
	clock       pub.Clock
 | 
						clock       pub.Clock
 | 
				
			||||||
	client      *http.Client
 | 
						client      *http.Client
 | 
				
			||||||
@@ -47,13 +49,14 @@ type Client struct {
 | 
				
			|||||||
	getHeaders  []string
 | 
						getHeaders  []string
 | 
				
			||||||
	postHeaders []string
 | 
						postHeaders []string
 | 
				
			||||||
	priv        *rsa.PrivateKey
 | 
						priv        *rsa.PrivateKey
 | 
				
			||||||
	pubId       string
 | 
						pubID       string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewClient(user *user_model.User, pubId string) (c *Client, err error) {
 | 
					// NewClient function
 | 
				
			||||||
	if err = containsRequiredHttpHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
 | 
					func NewClient(user *user_model.User, pubID string) (c *Client, err error) {
 | 
				
			||||||
 | 
						if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if err = containsRequiredHttpHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
 | 
						} else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	} else if !httpsig.IsSupportedDigestAlgorithm(setting.Federation.DigestAlgorithm) {
 | 
						} else if !httpsig.IsSupportedDigestAlgorithm(setting.Federation.DigestAlgorithm) {
 | 
				
			||||||
		err = fmt.Errorf("unsupported digest algorithm: %s", 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,
 | 
							getHeaders:  setting.Federation.GetHeaders,
 | 
				
			||||||
		postHeaders: setting.Federation.PostHeaders,
 | 
							postHeaders: setting.Federation.PostHeaders,
 | 
				
			||||||
		priv:        privParsed,
 | 
							priv:        privParsed,
 | 
				
			||||||
		pubId:       pubId,
 | 
							pubID:       pubID,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return
 | 
						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))
 | 
						byteCopy := make([]byte, len(b))
 | 
				
			||||||
	copy(byteCopy, b)
 | 
						copy(byteCopy, b)
 | 
				
			||||||
	buf := bytes.NewBuffer(byteCopy)
 | 
						buf := bytes.NewBuffer(byteCopy)
 | 
				
			||||||
	var req *http.Request
 | 
					 | 
				
			||||||
	req, err = http.NewRequest(http.MethodPost, to, buf)
 | 
						req, err = http.NewRequest(http.MethodPost, to, buf)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	req.Header.Add("Content-Type", activityStreamsContentType)
 | 
						req.Header.Add("Content-Type", ActivityStreamsContentType)
 | 
				
			||||||
	req.Header.Add("Accept-Charset", "utf-8")
 | 
						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")))
 | 
						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 {
 | 
						if err != nil {
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	err = signer.SignRequest(c.priv, c.pubId, req, b)
 | 
						err = signer.SignRequest(c.priv, c.pubID, req, b)
 | 
				
			||||||
	if err != nil {
 | 
						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
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	resp, err = c.client.Do(req)
 | 
						resp, err = c.client.Do(req)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,6 @@ package activitypub
 | 
				
			|||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io"
 | 
						"io"
 | 
				
			||||||
	"io/ioutil"
 | 
					 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"net/http/httptest"
 | 
						"net/http/httptest"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
@@ -24,16 +23,16 @@ import (
 | 
				
			|||||||
func TestActivityPubSignedPost(t *testing.T) {
 | 
					func TestActivityPubSignedPost(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}).(*user_model.User)
 | 
				
			||||||
	pubId := "https://example.com/pubId"
 | 
						pubID := "https://example.com/pubID"
 | 
				
			||||||
	c, err := NewClient(user, pubId)
 | 
						c, err := NewClient(user, pubID)
 | 
				
			||||||
	assert.NoError(t, err)
 | 
						assert.NoError(t, err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	expected := "BODY"
 | 
						expected := "BODY"
 | 
				
			||||||
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 | 
						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.Regexp(t, regexp.MustCompile("^"+setting.Federation.DigestAlgorithm), r.Header.Get("Digest"))
 | 
				
			||||||
		assert.Contains(t, r.Header.Get("Signature"), pubId)
 | 
							assert.Contains(t, r.Header.Get("Signature"), pubID)
 | 
				
			||||||
		assert.Equal(t, r.Header.Get("Content-Type"), activityStreamsContentType)
 | 
							assert.Equal(t, r.Header.Get("Content-Type"), ActivityStreamsContentType)
 | 
				
			||||||
		body, err := ioutil.ReadAll(r.Body)
 | 
							body, err := io.ReadAll(r.Body)
 | 
				
			||||||
		assert.NoError(t, err)
 | 
							assert.NoError(t, err)
 | 
				
			||||||
		assert.Equal(t, expected, string(body))
 | 
							assert.Equal(t, expected, string(body))
 | 
				
			||||||
		fmt.Fprintf(w, expected)
 | 
							fmt.Fprintf(w, expected)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
package structs
 | 
					package structs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ActivityPub type
 | 
				
			||||||
type ActivityPub struct {
 | 
					type ActivityPub struct {
 | 
				
			||||||
	Context string `json:"@context"`
 | 
						Context string `json:"@context"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@ import (
 | 
				
			|||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"code.gitea.io/gitea/models"
 | 
					 | 
				
			||||||
	"code.gitea.io/gitea/modules/activitypub"
 | 
						"code.gitea.io/gitea/modules/activitypub"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/context"
 | 
						"code.gitea.io/gitea/modules/context"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
@@ -17,32 +16,9 @@ import (
 | 
				
			|||||||
	"github.com/go-fed/activity/streams"
 | 
						"github.com/go-fed/activity/streams"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// hack waiting on https://github.com/go-gitea/gitea/pull/16834
 | 
					// Person function
 | 
				
			||||||
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
 | 
					 | 
				
			||||||
func Person(ctx *context.APIContext) {
 | 
					func Person(ctx *context.APIContext) {
 | 
				
			||||||
	// swagger:operation GET /activitypub/user/{username} information
 | 
						// swagger:operation GET /activitypub/user/{username} activitypub activitypubPerson
 | 
				
			||||||
	// ---
 | 
						// ---
 | 
				
			||||||
	// summary: Returns the person
 | 
						// summary: Returns the person
 | 
				
			||||||
	// produces:
 | 
						// produces:
 | 
				
			||||||
@@ -73,30 +49,30 @@ func Person(ctx *context.APIContext) {
 | 
				
			|||||||
	person.SetActivityStreamsName(name)
 | 
						person.SetActivityStreamsName(name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ibox := streams.NewActivityStreamsInboxProperty()
 | 
						ibox := streams.NewActivityStreamsInboxProperty()
 | 
				
			||||||
	url_object, _ := url.Parse(link + "/inbox")
 | 
						urlObject, _ := url.Parse(link + "/inbox")
 | 
				
			||||||
	ibox.SetIRI(url_object)
 | 
						ibox.SetIRI(urlObject)
 | 
				
			||||||
	person.SetActivityStreamsInbox(ibox)
 | 
						person.SetActivityStreamsInbox(ibox)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	obox := streams.NewActivityStreamsOutboxProperty()
 | 
						obox := streams.NewActivityStreamsOutboxProperty()
 | 
				
			||||||
	url_object, _ = url.Parse(link + "/outbox")
 | 
						urlObject, _ = url.Parse(link + "/outbox")
 | 
				
			||||||
	obox.SetIRI(url_object)
 | 
						obox.SetIRI(urlObject)
 | 
				
			||||||
	person.SetActivityStreamsOutbox(obox)
 | 
						person.SetActivityStreamsOutbox(obox)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
 | 
						publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	publicKeyType := streams.NewW3IDSecurityV1PublicKey()
 | 
						publicKeyType := streams.NewW3IDSecurityV1PublicKey()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	pubKeyIdProp := streams.NewJSONLDIdProperty()
 | 
						pubKeyIDProp := streams.NewJSONLDIdProperty()
 | 
				
			||||||
	pubKeyIRI, _ := url.Parse(link + "/#main-key")
 | 
						pubKeyIRI, _ := url.Parse(link + "/#main-key")
 | 
				
			||||||
	pubKeyIdProp.SetIRI(pubKeyIRI)
 | 
						pubKeyIDProp.SetIRI(pubKeyIRI)
 | 
				
			||||||
	publicKeyType.SetJSONLDId(pubKeyIdProp)
 | 
						publicKeyType.SetJSONLDId(pubKeyIDProp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	ownerProp := streams.NewW3IDSecurityV1OwnerProperty()
 | 
						ownerProp := streams.NewW3IDSecurityV1OwnerProperty()
 | 
				
			||||||
	ownerProp.SetIRI(idIRI)
 | 
						ownerProp.SetIRI(idIRI)
 | 
				
			||||||
	publicKeyType.SetW3IDSecurityV1Owner(ownerProp)
 | 
						publicKeyType.SetW3IDSecurityV1Owner(ownerProp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
 | 
						publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty()
 | 
				
			||||||
	if publicKeyPem, err := GetPublicKey(user); err != nil {
 | 
						if publicKeyPem, err := activitypub.GetPublicKey(user); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "GetPublicKey", err)
 | 
							ctx.Error(http.StatusInternalServerError, "GetPublicKey", err)
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
		publicKeyPemProp.Set(publicKeyPem)
 | 
							publicKeyPemProp.Set(publicKeyPem)
 | 
				
			||||||
@@ -110,3 +86,24 @@ func Person(ctx *context.APIContext) {
 | 
				
			|||||||
	jsonmap, _ = streams.Serialize(person)
 | 
						jsonmap, _ = streams.Serialize(person)
 | 
				
			||||||
	ctx.JSON(http.StatusOK, jsonmap)
 | 
						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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										158
									
								
								routers/api/v1/activitypub/reqsignature.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								routers/api/v1/activitypub/reqsignature.go
									
									
									
									
									
										Normal file
									
								
							@@ -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.Group("/user/{username}", func() {
 | 
				
			||||||
					m.Get("", activitypub.Person)
 | 
										m.Get("", activitypub.Person)
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
 | 
									m.Post("/user/{username}/inbox", activitypub.ReqSignature(), activitypub.PersonInbox)
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		m.Get("/signing-key.gpg", misc.SigningKey)
 | 
							m.Get("/signing-key.gpg", misc.SigningKey)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,8 +28,11 @@
 | 
				
			|||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
          "application/json"
 | 
					          "application/json"
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
					        "tags": [
 | 
				
			||||||
 | 
					          "activitypub"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
        "summary": "Returns the person",
 | 
					        "summary": "Returns the person",
 | 
				
			||||||
        "operationId": "information",
 | 
					        "operationId": "activitypubPerson",
 | 
				
			||||||
        "parameters": [
 | 
					        "parameters": [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            "type": "string",
 | 
					            "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": {
 | 
					    "/admin/cron": {
 | 
				
			||||||
      "get": {
 | 
					      "get": {
 | 
				
			||||||
        "produces": [
 | 
					        "produces": [
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user