mirror of https://github.com/go-gitea/gitea.git
activitypub: implement the ReqSignature middleware
Signed-off-by: Loïc Dachary <loic@dachary.org>
This commit is contained in:
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)
|
||||||
|
}
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
Loading…
Reference in New Issue