From bb8cace4622923839073112f687d52b5932844af Mon Sep 17 00:00:00 2001 From: DevMiner Date: Sun, 22 Sep 2024 01:09:05 +0200 Subject: [PATCH] feat: implement support for instance signed requests --- .../val_impls/request_validator_impl.go | 35 +++++++++++++++---- pkg/versia/crypto/federation_headers.go | 27 ++++++++++---- pkg/versia/crypto/federation_headers_test.go | 13 +++++++ 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/internal/validators/val_impls/request_validator_impl.go b/internal/validators/val_impls/request_validator_impl.go index 3fd4350..773c750 100644 --- a/internal/validators/val_impls/request_validator_impl.go +++ b/internal/validators/val_impls/request_validator_impl.go @@ -15,7 +15,8 @@ import ( ) var ( - ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidOriginHeader = errors.New("invalid origin header") _ validators.RequestValidator = (*RequestValidatorImpl)(nil) ) @@ -41,6 +42,13 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err defer s.End() ctx = s.Context() + origin := r.Header.Get("Origin") + if origin != "" { + return ErrInvalidOriginHeader + } + + l := i.log.WithValues("url", r.URL.Path) + r = r.WithContext(ctx) fedHeaders, err := versiacrypto.ExtractFederationHeaders(r.Header) @@ -48,23 +56,36 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err return err } - user, err := i.repositories.Users().Resolve(ctx, versiautils.URLFromStd(fedHeaders.SignedBy)) - if err != nil { - return err + var key *versiacrypto.SPKIPublicKey + var signerURI *versiautils.URL + if fedHeaders.SignedBy == nil { + metadata, err := i.repositories.InstanceMetadata().Resolve(ctx, origin) + if err != nil { + return err + } + signerURI = metadata.URI + } else { + user, err := i.repositories.Users().Resolve(ctx, versiautils.URLFromStd(fedHeaders.SignedBy)) + if err != nil { + return err + } + signerURI = user.URI } + l = l.WithValues("signer", signerURI) + body, err := utils.CopyBody(r) if err != nil { return err } - if !(versiacrypto.Verifier{PublicKey: user.PublicKey.Key}).Verify(r.Method, r.URL, body, fedHeaders) { - i.log.WithCallDepth(1).Info("signature verification failed", "user", user.URI, "url", r.URL.Path) + if !(versiacrypto.Verifier{PublicKey: key}).Verify(r.Method, r.URL, body, fedHeaders) { + l.WithCallDepth(1).Info("signature verification failed") s.CaptureError(ErrInvalidSignature) return ErrInvalidSignature } else { - i.log.V(2).Info("signature verification succeeded", "user", user.URI, "url", r.URL.Path) + l.V(2).Info("signature verification succeeded") } return nil diff --git a/pkg/versia/crypto/federation_headers.go b/pkg/versia/crypto/federation_headers.go index 6f10eec..04e57b4 100644 --- a/pkg/versia/crypto/federation_headers.go +++ b/pkg/versia/crypto/federation_headers.go @@ -11,7 +11,7 @@ import ( // // [Spec]: https://versia.pub/signatures#signature-definition type FederationHeaders struct { - // SignedBy is the URL to a user + // SignedBy is the URL to a user, or `nil` when the signature was created by the instance's privatekey SignedBy *url.URL // Nonce is a random string, used to prevent replay attacks Nonce string @@ -20,14 +20,23 @@ type FederationHeaders struct { } func (f *FederationHeaders) Inject(h http.Header) { - h.Set("x-signed-by", f.SignedBy.String()) + signedBy := "instance" + if f.SignedBy != nil { + signedBy = f.SignedBy.String() + } + h.Set("x-signed-by", signedBy) h.Set("x-nonce", f.Nonce) h.Set("x-signature", base64.StdEncoding.EncodeToString(f.Signature)) } func (f *FederationHeaders) Headers() map[string]string { + signedBy := "instance" + if f.SignedBy != nil { + signedBy = f.SignedBy.String() + } + return map[string]string{ - "x-signed-by": f.SignedBy.String(), + "x-signed-by": signedBy, "x-nonce": f.Nonce, "x-signature": base64.StdEncoding.EncodeToString(f.Signature), } @@ -39,9 +48,15 @@ func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) { return nil, fmt.Errorf("missing x-signed-by header") } - u, err := url.Parse(signedBy) - if err != nil { - return nil, err + var u *url.URL + if signedBy == "instance" { + // Signed by the instance + } else { + var err error + u, err = url.Parse(signedBy) + if err != nil { + return nil, err + } } nonce := h.Get("x-nonce") diff --git a/pkg/versia/crypto/federation_headers_test.go b/pkg/versia/crypto/federation_headers_test.go index 15a415b..3ff2616 100644 --- a/pkg/versia/crypto/federation_headers_test.go +++ b/pkg/versia/crypto/federation_headers_test.go @@ -1,6 +1,7 @@ package versiacrypto import ( + "net/http" "net/url" "testing" @@ -17,3 +18,15 @@ func TestFederationHeaders_String(t *testing.T) { assert.Equal(t, "post /users/bob?z=foo&a=bar 1234567890 LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=", one.String()) } + +func TestFederationHeaders_Headers(t *testing.T) { + headers, err := ExtractFederationHeaders(http.Header{ + "X-Signed-By": []string{"instance"}, + "X-Nonce": []string{"11"}, + "X-Signature": []string{"Cg=="}, + }) + + assert.NoError(t, err) + + assert.Nil(t, headers.SignedBy, "the SignedBy field should be nil when the signer is the instance") +}