mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-20 01:36:37 +00:00
Adds support for verifying [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) HTTP message signatures. Adds support for signing HTTP requests using the [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421.html) algorithm. Adds support for multiple RFC 9421 signers. Only attempts to sign RSA v1.5 signatures, since they are the only signatures supported by Mastodon. However, this commit lays the groundwork for future support of additional signature algorithms. Adds a basic integration test case for RFC 9421 signing + verification. Adds unit test cases for `ClientKey` RSA keys. Adds unit tests for `ClientKey` ECDSA + Ed25519 key methods. Adds more unit test cases for RSA key methods. Adds basic unit tests for the `setting.Algorithm` enum type.
864 lines
25 KiB
Go
864 lines
25 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// TODO: Think about whether this should be moved to services/activitypub (compare to exosy/services/activitypub/client.go)
|
|
package activitypub
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/ed25519"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
forgefed_model "forgejo.org/models/forgefed"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/proxy"
|
|
"forgejo.org/modules/setting"
|
|
|
|
"github.com/42wim/httpsig"
|
|
httpsign9421 "github.com/yaronf/httpsign"
|
|
)
|
|
|
|
const (
|
|
// ActivityStreamsContentType const
|
|
ActivityStreamsContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
|
httpsigExpirationTime = 60
|
|
)
|
|
|
|
func CurrentTime() string {
|
|
return time.Now().UTC().Format(http.TimeFormat)
|
|
}
|
|
|
|
func containsRequiredHTTPHeaders(method string, headers []string) error {
|
|
var hasRequestTarget, hasDate, hasDigest, hasHost bool
|
|
for _, header := range headers {
|
|
hasRequestTarget = hasRequestTarget || header == httpsig.RequestTarget
|
|
hasDate = hasDate || header == "Date"
|
|
hasDigest = hasDigest || header == "Digest"
|
|
hasHost = hasHost || header == "Host"
|
|
}
|
|
if !hasRequestTarget {
|
|
return fmt.Errorf("missing http header for %s: %s", method, httpsig.RequestTarget)
|
|
} else if !hasDate {
|
|
return fmt.Errorf("missing http header for %s: Date", method)
|
|
} else if !hasHost {
|
|
return fmt.Errorf("missing http header for %s: Host", method)
|
|
} else if !hasDigest && method != http.MethodGet {
|
|
return fmt.Errorf("missing http header for %s: Digest", method)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Client struct
|
|
type ClientFactory struct {
|
|
client *http.Client
|
|
algs []httpsig.Algorithm
|
|
digestAlg httpsig.DigestAlgorithm
|
|
getHeaders []string
|
|
postHeaders []string
|
|
}
|
|
|
|
// NewClient function
|
|
func NewClientFactory() (c *ClientFactory, err error) {
|
|
return NewClientFactoryWithTimeout(5 * time.Second)
|
|
}
|
|
|
|
// NewClient function
|
|
func NewClientFactoryWithTimeout(timeout time.Duration) (c *ClientFactory, err error) {
|
|
if err = containsRequiredHTTPHeaders(http.MethodGet, setting.Federation.GetHeaders); err != nil {
|
|
return nil, err
|
|
} else if err = containsRequiredHTTPHeaders(http.MethodPost, setting.Federation.PostHeaders); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c = &ClientFactory{
|
|
client: &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: proxy.Proxy(),
|
|
},
|
|
Timeout: timeout,
|
|
},
|
|
algs: setting.HttpsigAlgs,
|
|
digestAlg: httpsig.DigestAlgorithm(setting.Federation.DigestAlgorithm),
|
|
getHeaders: setting.Federation.GetHeaders,
|
|
postHeaders: setting.Federation.PostHeaders,
|
|
}
|
|
return c, err
|
|
}
|
|
|
|
type APClientFactory interface {
|
|
WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error)
|
|
WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error)
|
|
WithPublicKeys(ctx context.Context, keys []ClientPublicKey) (APClient, error)
|
|
}
|
|
|
|
// ClientPublicKey is a convenience struct used to construct a verifying ClientKey
|
|
type ClientPublicKey struct {
|
|
KeyID string
|
|
Alg setting.Algorithm
|
|
}
|
|
|
|
// NewClientPublicKey creates a new ClientPublicKey for verifying signatures.
|
|
func NewClientPublicKey(keyID string, alg setting.Algorithm) ClientPublicKey {
|
|
return ClientPublicKey{keyID, alg}
|
|
}
|
|
|
|
func (k ClientPublicKey) PublicKeyBytes(ctx context.Context) ([]byte, error) {
|
|
_, federatedUser, err := user_model.FindFederatedUserByKeyID(ctx, k.KeyID)
|
|
if err == nil && federatedUser != nil && federatedUser.PublicKey.Valid {
|
|
return federatedUser.PublicKey.V, nil
|
|
} else if host, err := forgefed_model.FindFederationHostByKeyID(ctx, k.KeyID); err == nil && host != nil && host.PublicKey.Valid {
|
|
return host.PublicKey.V, nil
|
|
}
|
|
return nil, fmt.Errorf("error finding public key for key ID: %v, error: %v", k.KeyID, err)
|
|
}
|
|
|
|
// ClientKey creates a new [ClientKey].
|
|
func (k ClientPublicKey) ClientKey(ctx context.Context) (*ClientKey, error) {
|
|
keyBytes, err := k.PublicKeyBytes(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientKey := &ClientKey{
|
|
pubKey: keyBytes,
|
|
pubKeyID: k.KeyID,
|
|
alg: k.Alg,
|
|
}
|
|
return clientKey, nil
|
|
}
|
|
|
|
// ClientKey represents a client key used to sign a HTTP request.
|
|
type ClientKey struct {
|
|
privKey []byte
|
|
pubKey []byte
|
|
pubKeyID string
|
|
alg setting.Algorithm
|
|
}
|
|
|
|
// NewClientKey creates a new [ClientKey] from the provided parameters.
|
|
func NewClientKey(privKey []byte, pubKeyID string, alg setting.Algorithm) *ClientKey {
|
|
return &ClientKey{
|
|
privKey: privKey,
|
|
pubKeyID: pubKeyID,
|
|
alg: alg,
|
|
}
|
|
}
|
|
|
|
// RSAPrivateKey attempts to parse an RSA private key from the [ClientKey].
|
|
func (k ClientKey) RSAPrivateKey() (*rsa.PrivateKey, error) {
|
|
switch k.alg {
|
|
case setting.AlgorithmRSARFC9421, setting.AlgorithmRSAPSSRFC9421, setting.AlgorithmRSASHA256CAVAGE, setting.AlgorithmRSASHA512CAVAGE:
|
|
privPem, _ := pem.Decode(k.privKey)
|
|
return x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
|
default:
|
|
return nil, fmt.Errorf("invalid signing algorithm: %v", k.alg)
|
|
}
|
|
}
|
|
|
|
// RSAPublicKey attempts to parse an RSA private key from the [ClientKey].
|
|
func (k ClientKey) RSAPublicKey() (*rsa.PublicKey, error) {
|
|
if k.privKey == nil {
|
|
switch k.alg {
|
|
case setting.AlgorithmRSARFC9421, setting.AlgorithmRSAPSSRFC9421, setting.AlgorithmRSASHA256CAVAGE, setting.AlgorithmRSASHA512CAVAGE:
|
|
key, err := x509.ParsePKIXPublicKey(k.pubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, ok := key.(*rsa.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid RSA public key")
|
|
}
|
|
return pk, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid signing algorithm: %v", k.alg)
|
|
}
|
|
} else {
|
|
priv, err := k.RSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, ok := priv.Public().(*rsa.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid RSA public key")
|
|
}
|
|
return pk, nil
|
|
}
|
|
}
|
|
|
|
// ECDSAPrivateKey attempts to parse an ECDSA private key from the [ClientKey].
|
|
func (k ClientKey) ECDSAPrivateKey() (*ecdsa.PrivateKey, error) {
|
|
switch k.alg {
|
|
case setting.AlgorithmP256CAVAGE, setting.AlgorithmP256RFC9421, setting.AlgorithmP384CAVAGE, setting.AlgorithmP384RFC9421:
|
|
privPem, _ := pem.Decode(k.privKey)
|
|
pk, err := x509.ParsePKCS8PrivateKey(privPem.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ecdsaPriv, ok := pk.(*ecdsa.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid ECDSA P256 private key")
|
|
}
|
|
return ecdsaPriv, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid ECDSA algorithm: %v", k.alg)
|
|
}
|
|
}
|
|
|
|
// ECDSAPublicKey attempts to parse an ECDSA public key from the [ClientKey].
|
|
func (k ClientKey) ECDSAPublicKey() (*ecdsa.PublicKey, error) {
|
|
if k.privKey == nil {
|
|
pk, err := x509.ParsePKIXPublicKey(k.pubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch k.alg {
|
|
case setting.AlgorithmP256CAVAGE, setting.AlgorithmP256RFC9421,
|
|
setting.AlgorithmP384CAVAGE, setting.AlgorithmP384RFC9421:
|
|
k, ok := pk.(*ecdsa.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid ECDSA public key")
|
|
}
|
|
return k, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid ECDSA algorithm: %v", k.alg)
|
|
}
|
|
} else {
|
|
priv, err := k.ECDSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, ok := priv.Public().(*ecdsa.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid ECDSA public key")
|
|
}
|
|
return pk, nil
|
|
}
|
|
}
|
|
|
|
// Ed25519PrivateKey attempts to parse an Ed25519 private key from the [ClientKey].
|
|
func (k ClientKey) Ed25519PrivateKey() (*ed25519.PrivateKey, error) {
|
|
if k.privKey == nil {
|
|
return nil, fmt.Errorf("nil private key")
|
|
}
|
|
|
|
switch k.alg {
|
|
case setting.AlgorithmEd25519:
|
|
privPem, _ := pem.Decode(k.privKey)
|
|
pk, err := x509.ParsePKCS8PrivateKey(privPem.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
k, ok := pk.(ed25519.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid Ed25519 private key")
|
|
}
|
|
return &k, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid Ed25519 algorithm: %v", k.alg)
|
|
}
|
|
}
|
|
|
|
// Ed25519PublicKey attempts to parse an Ed25519 public key from the [ClientKey].
|
|
func (k ClientKey) Ed25519PublicKey() (*ed25519.PublicKey, error) {
|
|
if k.privKey == nil {
|
|
switch k.alg {
|
|
case setting.AlgorithmEd25519:
|
|
pk, err := x509.ParsePKIXPublicKey(k.pubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
k, ok := pk.(ed25519.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid Ed25519 public key")
|
|
}
|
|
return &k, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid Ed25519 algorithm: %v", k.alg)
|
|
}
|
|
} else {
|
|
priv, err := k.Ed25519PrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pk, ok := priv.Public().(ed25519.PublicKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid Ed25519 public key")
|
|
}
|
|
return &pk, nil
|
|
}
|
|
}
|
|
|
|
type RFC9421Config struct {
|
|
Signer *httpsign9421.SignConfig
|
|
Verifier *httpsign9421.VerifyConfig
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421Ed25519(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
pk, err := k.Ed25519PrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewEd25519Signer(*pk, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421Ed25519(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
pk, err := k.Ed25519PublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewEd25519Verifier(*pk, config.Verifier, fields)
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421HMACSHA256(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
return httpsign9421.NewHMACSHA256Signer(k.privKey, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421HMACSHA256(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
return httpsign9421.NewHMACSHA256Verifier(k.pubKey, config.Verifier, fields)
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421P256(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
pk, err := k.ECDSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewP256Signer(*pk, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421P256(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
pk, err := k.ECDSAPublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewP256Verifier(*pk, config.Verifier, fields)
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421P384(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
pk, err := k.ECDSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewP384Signer(*pk, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421P384(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
pk, err := k.ECDSAPublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewP384Verifier(*pk, config.Verifier, fields)
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421RSA(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
pk, err := k.RSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewRSASigner(*pk, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421RSA(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
pk, err := k.RSAPublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewRSAVerifier(*pk, config.Verifier, fields)
|
|
}
|
|
|
|
func (k ClientKey) SignerRFC9421RSAPSS(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
pk, err := k.RSAPrivateKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewRSAPSSSigner(*pk, config.Signer, fields)
|
|
}
|
|
|
|
func (k ClientKey) VerifierRFC9421RSAPSS(config RFC9421Config, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
pk, err := k.RSAPublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return httpsign9421.NewRSAPSSVerifier(*pk, config.Verifier, fields)
|
|
}
|
|
|
|
// SignerRFC9421 attempts to create a valid RFC 9421 HTTP Message signer.
|
|
//
|
|
|
|
func (k ClientKey) SignerRFC9421(config *httpsign9421.SignConfig, fields httpsign9421.Fields) (*httpsign9421.Signer, error) {
|
|
if config == nil {
|
|
return nil, fmt.Errorf("nil signer config")
|
|
}
|
|
|
|
config.SetKeyID(k.pubKeyID)
|
|
|
|
signConfig := RFC9421Config{
|
|
Signer: config,
|
|
}
|
|
|
|
switch k.alg {
|
|
case setting.AlgorithmEd25519:
|
|
return k.SignerRFC9421Ed25519(signConfig, fields)
|
|
case setting.AlgorithmHMACSHA256:
|
|
return k.SignerRFC9421HMACSHA256(signConfig, fields)
|
|
case setting.AlgorithmP256RFC9421:
|
|
return k.SignerRFC9421P256(signConfig, fields)
|
|
case setting.AlgorithmP384RFC9421:
|
|
return k.SignerRFC9421P384(signConfig, fields)
|
|
case setting.AlgorithmRSARFC9421:
|
|
return k.SignerRFC9421RSA(signConfig, fields)
|
|
case setting.AlgorithmRSAPSSRFC9421:
|
|
return k.SignerRFC9421RSAPSS(signConfig, fields)
|
|
default:
|
|
return nil, fmt.Errorf("invalid RFC 9421 signature algorithm: %v", k.alg)
|
|
}
|
|
}
|
|
|
|
// VerifierRFC9421 attempts to create a valid RFC 9421 HTTP Message verifier.
|
|
//
|
|
|
|
func (k ClientKey) VerifierRFC9421(config *httpsign9421.VerifyConfig, fields httpsign9421.Fields) (*httpsign9421.Verifier, error) {
|
|
if config == nil {
|
|
return nil, fmt.Errorf("nil verifier config")
|
|
}
|
|
|
|
config.SetKeyID(k.pubKeyID)
|
|
|
|
verifyConfig := RFC9421Config{
|
|
Verifier: config,
|
|
}
|
|
|
|
switch k.alg {
|
|
case setting.AlgorithmEd25519:
|
|
return k.VerifierRFC9421Ed25519(verifyConfig, fields)
|
|
case setting.AlgorithmHMACSHA256:
|
|
return k.VerifierRFC9421HMACSHA256(verifyConfig, fields)
|
|
case setting.AlgorithmP256RFC9421:
|
|
return k.VerifierRFC9421P256(verifyConfig, fields)
|
|
case setting.AlgorithmP384RFC9421:
|
|
return k.VerifierRFC9421P384(verifyConfig, fields)
|
|
case setting.AlgorithmRSARFC9421:
|
|
return k.VerifierRFC9421RSA(verifyConfig, fields)
|
|
case setting.AlgorithmRSAPSSRFC9421:
|
|
return k.VerifierRFC9421RSAPSS(verifyConfig, fields)
|
|
default:
|
|
return nil, fmt.Errorf("invalid RFC 9421 signature algorithm: %v", k.alg)
|
|
}
|
|
}
|
|
|
|
// Client struct
|
|
type Client struct {
|
|
client *http.Client
|
|
algs []httpsig.Algorithm
|
|
digestAlg httpsig.DigestAlgorithm
|
|
getHeaders []string
|
|
postHeaders []string
|
|
priv *rsa.PrivateKey
|
|
pubID string
|
|
clientKeys []*ClientKey
|
|
useRFC9421 bool
|
|
}
|
|
|
|
// NewRequest function
|
|
func (cf *ClientFactory) WithKeysDirect(ctx context.Context, privateKey, pubID string) (APClient, error) {
|
|
privPem, _ := pem.Decode([]byte(privateKey))
|
|
privParsed, err := x509.ParsePKCS1PrivateKey(privPem.Bytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clientKeys := []*ClientKey{
|
|
NewClientKey([]byte(privateKey), pubID, setting.AlgorithmRSASHA256CAVAGE),
|
|
NewClientKey([]byte(privateKey), pubID, setting.AlgorithmRSARFC9421),
|
|
}
|
|
|
|
c := Client{
|
|
client: cf.client,
|
|
algs: cf.algs,
|
|
digestAlg: cf.digestAlg,
|
|
getHeaders: cf.getHeaders,
|
|
postHeaders: cf.postHeaders,
|
|
priv: privParsed,
|
|
pubID: pubID,
|
|
clientKeys: clientKeys,
|
|
useRFC9421: setting.Federation.UseRFC9421,
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
func (cf *ClientFactory) WithKeys(ctx context.Context, user *user_model.User, pubID string) (APClient, error) {
|
|
priv, err := GetPrivateKey(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cf.WithKeysDirect(ctx, priv, pubID)
|
|
}
|
|
|
|
// WithPublicKeys creates a new [Client] for verifying signatures.
|
|
func (cf *ClientFactory) WithPublicKeys(ctx context.Context, pubKeys []ClientPublicKey) (APClient, error) {
|
|
clientKeys := make([]*ClientKey, len(pubKeys))
|
|
|
|
for i, key := range pubKeys {
|
|
clientKey, err := key.ClientKey(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
clientKeys[i] = clientKey
|
|
}
|
|
|
|
c := Client{
|
|
client: cf.client,
|
|
algs: cf.algs,
|
|
digestAlg: cf.digestAlg,
|
|
getHeaders: cf.getHeaders,
|
|
postHeaders: cf.postHeaders,
|
|
clientKeys: clientKeys,
|
|
useRFC9421: setting.Federation.UseRFC9421,
|
|
}
|
|
return &c, nil
|
|
}
|
|
|
|
// NewRequest function
|
|
func (c *Client) newRequest(method string, b []byte, to string) (req *http.Request, err error) {
|
|
buf := bytes.NewBuffer(b)
|
|
req, err = http.NewRequest(method, to, buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Add("Accept", "application/json, "+ActivityStreamsContentType)
|
|
req.Header.Add("Date", CurrentTime())
|
|
req.Header.Add("Host", req.URL.Host)
|
|
req.Header.Add("User-Agent", "Gitea/"+setting.AppVer)
|
|
req.Header.Add("Content-Type", ActivityStreamsContentType)
|
|
|
|
return req, err
|
|
}
|
|
|
|
// SignersRFC9421 attempts to create a list of valid RFC 9421 HTTP Message signers.
|
|
func (c *Client) SignersRFC9421(config *httpsign9421.SignConfig, fields httpsign9421.Fields) ([]httpsign9421.Signer, error) {
|
|
if config == nil {
|
|
return nil, fmt.Errorf("nil signer config")
|
|
}
|
|
var (
|
|
err error
|
|
signer *httpsign9421.Signer
|
|
signers []httpsign9421.Signer
|
|
)
|
|
for _, key := range c.clientKeys {
|
|
signer, err = key.SignerRFC9421(config, fields)
|
|
if err != nil {
|
|
log.Warn("Invalid RFC 9421 signer: %v", err)
|
|
continue
|
|
}
|
|
signers = append(signers, *signer)
|
|
}
|
|
|
|
return signers, nil
|
|
}
|
|
|
|
// ContentDigest calculates the SHA256 + SHA512 content-digest header value.
|
|
func ContentDigest(b *io.ReadCloser, digestAlgs []string) (string, error) {
|
|
digest, err := httpsign9421.GenerateContentDigestHeader(b, digestAlgs)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error generating RFC 9421 content-digest: %v", err)
|
|
}
|
|
return digest, nil
|
|
}
|
|
|
|
// ValidateContentDigest validates the `Content-Digest` header.
|
|
//
|
|
// `digestAlgs` represents the list of expected digest algorithms, e.g. "sha-256", "sha-512"
|
|
func ValidateContentDigest(header string, b *io.ReadCloser, digestAlgs []string) error {
|
|
if b == nil {
|
|
return fmt.Errorf("nil content-digest body I/O reader")
|
|
}
|
|
body := bytes.NewBufferString("")
|
|
if _, err := body.ReadFrom(*b); err != nil {
|
|
return err
|
|
}
|
|
*b = io.NopCloser(bytes.NewBufferString(body.String()))
|
|
bodyReader := io.NopCloser(body)
|
|
|
|
return httpsign9421.ValidateContentDigestHeader([]string{header}, &bodyReader, digestAlgs)
|
|
}
|
|
|
|
// Do makes an HTTP request to an ActivityPub server.
|
|
func (c *Client) Do(req *http.Request) (resp *http.Response, err error) {
|
|
if req == nil {
|
|
return nil, fmt.Errorf("nil ActivityPub request")
|
|
}
|
|
|
|
return c.client.Do(req)
|
|
}
|
|
|
|
// Post constructs a POST request with forgejo/gitea specific headers, and makes the request to the server.
|
|
func (c *Client) Post(b []byte, to string) (resp *http.Response, err error) {
|
|
req, err := c.PostRequest(b, to)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.Do(req)
|
|
}
|
|
|
|
// KeyID gets the key ID for the ActivityPub public key.
|
|
func (c *Client) KeyID() string {
|
|
return c.pubID
|
|
}
|
|
|
|
// Create an http POST request with forgejo/gitea specific headers
|
|
//
|
|
//nolint:dupl
|
|
func (c *Client) PostRequest(b []byte, to string) (req *http.Request, err error) {
|
|
if req, err = c.newRequest(http.MethodPost, b, to); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.pubID != "" {
|
|
if c.useRFC9421 {
|
|
config := httpsign9421.NewSignConfig().SignCreated(true)
|
|
fields := httpsign9421.Headers(setting.Federation.PostHeadersRFC9421...)
|
|
|
|
signers, err := c.SignersRFC9421(config, fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Created", fmt.Sprintf("%d", time.Now().Unix()))
|
|
digest, err := ContentDigest(&req.Body, setting.Federation.DigestAlgorithms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Digest", digest)
|
|
|
|
for i, signer := range signers {
|
|
sigName := fmt.Sprintf("sig%d", i+1)
|
|
signatureInput, signature, err := httpsign9421.SignRequest(sigName, signer, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Signature-Input", signatureInput)
|
|
req.Header.Set("Signature", signature)
|
|
}
|
|
} else {
|
|
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.postHeaders, httpsig.Signature, httpsigExpirationTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := signer.SignRequest(c.priv, c.pubID, req, b); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// Create an http GET request with forgejo/gitea specific headers
|
|
func (c *Client) Get(to string) (resp *http.Response, err error) {
|
|
req, err := c.GetRequest(to)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return c.Do(req)
|
|
}
|
|
|
|
// Create an http GET request with forgejo/gitea specific headers
|
|
//
|
|
//nolint:dupl
|
|
func (c *Client) GetRequest(to string) (req *http.Request, err error) {
|
|
if req, err = c.newRequest(http.MethodGet, nil, to); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if c.pubID != "" {
|
|
if c.useRFC9421 {
|
|
config := httpsign9421.NewSignConfig().SignCreated(true)
|
|
fields := httpsign9421.Headers(setting.Federation.GetHeadersRFC9421...)
|
|
|
|
signers, err := c.SignersRFC9421(config, fields)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Created", fmt.Sprintf("%d", time.Now().Unix()))
|
|
digest, err := ContentDigest(&req.Body, setting.Federation.DigestAlgorithms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Content-Digest", digest)
|
|
|
|
for i, signer := range signers {
|
|
sigName := fmt.Sprintf("sig%d", i+1)
|
|
signatureInput, signature, err := httpsign9421.SignRequest(sigName, signer, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Signature-Input", signatureInput)
|
|
req.Header.Set("Signature", signature)
|
|
}
|
|
} else {
|
|
signer, _, err := httpsig.NewSigner(c.algs, c.digestAlg, c.getHeaders, httpsig.Signature, httpsigExpirationTime)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := signer.SignRequest(c.priv, c.pubID, req, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
return req, nil
|
|
}
|
|
|
|
// Create an http GET request with forgejo/gitea specific headers
|
|
func (c *Client) GetBody(uri string) ([]byte, error) {
|
|
response, err := c.Get(uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log.Debug("Client: got status: %v", response.Status)
|
|
if response.StatusCode != 200 {
|
|
err = fmt.Errorf("got non 200 status code for id: %v", uri)
|
|
return nil, err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.ContentLength > setting.Federation.MaxSize {
|
|
return nil, fmt.Errorf("Request returned %d bytes (max allowed incoming size: %d bytes)", response.ContentLength, setting.Federation.MaxSize)
|
|
} else if response.ContentLength == -1 {
|
|
log.Warn("Request to %v returned an unknown content length, response may be truncated to %d bytes", uri, setting.Federation.MaxSize)
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(response.Body, setting.Federation.MaxSize))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
log.Debug("Client: got body: %v", charLimiter(string(body), 120))
|
|
return body, nil
|
|
}
|
|
|
|
// SetRFC9421 sets whether to sign requests using the RFC 9421 HTTP Message Signing algorithm.
|
|
func (c *Client) SetRFC9421(use bool) {
|
|
c.useRFC9421 = use
|
|
}
|
|
|
|
// GetRFC9421 gets whether the RFC 9421 HTTP Message Signing algorithm is used to sign requests.
|
|
func (c *Client) GetRFC9421() bool {
|
|
return c.useRFC9421
|
|
}
|
|
|
|
func (c *Client) SignedHeaders(method string) string {
|
|
var ret string
|
|
|
|
switch method {
|
|
case http.MethodGet:
|
|
if c.GetRFC9421() {
|
|
ret = fmt.Sprintf(`"%v"`, strings.Join(setting.Federation.GetHeadersRFC9421, `" "`))
|
|
} else {
|
|
ret = strings.Join(setting.Federation.GetHeaders, " ")
|
|
}
|
|
case http.MethodPost:
|
|
if c.GetRFC9421() {
|
|
ret = fmt.Sprintf(`"%v"`, strings.Join(setting.Federation.PostHeadersRFC9421, `" "`))
|
|
} else {
|
|
ret = strings.Join(setting.Federation.PostHeaders, " ")
|
|
}
|
|
default:
|
|
ret = ""
|
|
}
|
|
|
|
return strings.ToLower(ret)
|
|
}
|
|
|
|
// Limit number of characters in a string (useful to prevent log injection attacks and overly long log outputs)
|
|
// Thanks to https://www.socketloop.com/tutorials/golang-characters-limiter-example
|
|
func charLimiter(s string, limit int) string {
|
|
reader := strings.NewReader(s)
|
|
buff := make([]byte, limit)
|
|
n, _ := io.ReadAtLeast(reader, buff, limit)
|
|
if n != 0 {
|
|
return fmt.Sprint(string(buff), "...")
|
|
}
|
|
return s
|
|
}
|
|
|
|
type APClient interface {
|
|
newRequest(method string, b []byte, to string) (req *http.Request, err error)
|
|
Post(b []byte, to string) (resp *http.Response, err error)
|
|
PostRequest(b []byte, to string) (req *http.Request, err error)
|
|
Get(to string) (resp *http.Response, err error)
|
|
GetRequest(to string) (req *http.Request, err error)
|
|
GetBody(uri string) ([]byte, error)
|
|
Do(req *http.Request) (resp *http.Response, err error)
|
|
GetRFC9421() bool
|
|
SetRFC9421(use bool)
|
|
KeyID() string
|
|
SignedHeaders(method string) string
|
|
}
|
|
|
|
// contextKey is a value for use with context.WithValue.
|
|
type contextKey struct {
|
|
name string
|
|
}
|
|
|
|
// clientFactoryContextKey is a context key. It is used with context.Value() to get the current Food for the context
|
|
var (
|
|
clientFactoryContextKey = &contextKey{"clientFactory"}
|
|
_ APClientFactory = &ClientFactory{}
|
|
)
|
|
|
|
// Context represents an activitypub client factory context
|
|
type Context struct {
|
|
context.Context
|
|
e APClientFactory
|
|
}
|
|
|
|
func NewContext(ctx context.Context, e APClientFactory) *Context {
|
|
return &Context{
|
|
Context: ctx,
|
|
e: e,
|
|
}
|
|
}
|
|
|
|
// APClientFactory represents an activitypub client factory
|
|
func (ctx *Context) APClientFactory() APClientFactory {
|
|
return ctx.e
|
|
}
|
|
|
|
// provides APClientFactory
|
|
type GetAPClient interface {
|
|
GetClientFactory() APClientFactory
|
|
}
|
|
|
|
// GetClientFactory will get an APClientFactory from this context or returns the default implementation
|
|
func GetClientFactory(ctx context.Context) (APClientFactory, error) {
|
|
if e := getClientFactory(ctx); e != nil {
|
|
return e, nil
|
|
}
|
|
return NewClientFactory()
|
|
}
|
|
|
|
// getClientFactory will get an APClientFactory from this context or return nil
|
|
func getClientFactory(ctx context.Context) APClientFactory {
|
|
if clientFactory, ok := ctx.(APClientFactory); ok {
|
|
return clientFactory
|
|
}
|
|
clientFactoryInterface := ctx.Value(clientFactoryContextKey)
|
|
if clientFactoryInterface != nil {
|
|
return clientFactoryInterface.(GetAPClient).GetClientFactory()
|
|
}
|
|
return nil
|
|
}
|