mirror of
https://github.com/versia-pub/versia-go.git
synced 2025-12-06 14:28:20 +01:00
refactor!: WD-4 signatures
This commit is contained in:
parent
bb2e69e982
commit
93a61a8f29
|
|
@ -44,13 +44,13 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err
|
||||||
|
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
date, sigHeader, err := lysand.ExtractFederationHeaders(r.Header)
|
fedHeaders, err := lysand.ExtractFederationHeaders(r.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fetch user from database instead of using the URI
|
// TODO: Fetch user from database instead of using the URI
|
||||||
user, err := i.repositories.Users().Resolve(ctx, lysand.URLFromStd(sigHeader.KeyID))
|
user, err := i.repositories.Users().Resolve(ctx, lysand.URLFromStd(fedHeaders.SignedBy))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -60,13 +60,13 @@ func (i RequestValidatorImpl) Validate(ctx context.Context, r *http.Request) err
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(lysand.Verifier{PublicKey: user.PublicKey}).Verify(r.Method, date, r.Host, r.URL.Path, body, sigHeader) {
|
if !(lysand.Verifier{PublicKey: user.PublicKey}).Verify(r.Method, r.URL, body, fedHeaders) {
|
||||||
i.log.Info("signature verification failed", "user", user.URI, "ur", r.URL.Path)
|
i.log.Info("signature verification failed", "user", user.URI, "url", r.URL.Path)
|
||||||
s.CaptureError(ErrInvalidSignature)
|
s.CaptureError(ErrInvalidSignature)
|
||||||
|
|
||||||
return ErrInvalidSignature
|
return ErrInvalidSignature
|
||||||
} else {
|
} else {
|
||||||
i.log.V(2).Info("signature verification succeeded", "user", user.URI, "ur", r.URL.Path)
|
i.log.V(2).Info("signature verification succeeded", "user", user.URI, "url", r.URL.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// User represents a user object in the Lysand protocol. For more information, see the [Spec].
|
// User represents a user object in the Lysand protocol. For more information, see the [Spec].
|
||||||
|
|
@ -105,13 +106,13 @@ func (c *FederationClient) GetUser(ctx context.Context, uri *url.URL) (*User, er
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
date, sigHeader, err := ExtractFederationHeaders(resp.Header)
|
fedHeaders, err := ExtractFederationHeaders(resp.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
||||||
if !v.Verify("GET", date, uri.Host, uri.Path, body, sigHeader) {
|
if !v.Verify("GET", uri, body, fedHeaders) {
|
||||||
c.log.V(2).Info("signature verification failed", "user", user.URI.String())
|
c.log.V(2).Info("signature verification failed", "user", user.URI.String())
|
||||||
return nil, fmt.Errorf("signature verification failed")
|
return nil, fmt.Errorf("signature verification failed")
|
||||||
}
|
}
|
||||||
|
|
@ -128,9 +129,12 @@ func (c *FederationClient) SendToInbox(ctx context.Context, signer Signer, user
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
date := time.Now()
|
nonce := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
sigData := NewSignatureData("POST", date, uri.Host, uri.Path, hashSHA256(body))
|
sigData := NewSignatureData("POST", base64.StdEncoding.EncodeToString(nonce), uri, hashSHA256(body))
|
||||||
sig := signer.Sign(*sigData)
|
sig := signer.Sign(*sigData)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
|
req, err := http.NewRequestWithContext(ctx, "POST", uri.String(), bytes.NewReader(body))
|
||||||
|
|
@ -138,8 +142,7 @@ func (c *FederationClient) SendToInbox(ctx context.Context, signer Signer, user
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Set("Date", TimeFromStd(date).String())
|
sig.Inject(req.Header)
|
||||||
req.Header.Set("Signature", sig.String())
|
|
||||||
|
|
||||||
_, respBody, err := c.doReq(req)
|
_, respBody, err := c.doReq(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,18 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *FederationClient) ValidateSignatureHeader(req *http.Request) (bool, error) {
|
func (c *FederationClient) ValidateSignatureHeader(req *http.Request) (bool, error) {
|
||||||
date, sigHeader, err := ExtractFederationHeaders(req.Header)
|
fedHeaders, err := ExtractFederationHeaders(req.Header)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Fetch user from database instead of using the URI
|
// TODO: Fetch user from database instead of using the URI
|
||||||
user, err := c.GetUser(req.Context(), sigHeader.KeyID)
|
user, err := c.GetUser(req.Context(), fedHeaders.SignedBy)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
@ -29,39 +26,11 @@ func (c *FederationClient) ValidateSignatureHeader(req *http.Request) (bool, err
|
||||||
}
|
}
|
||||||
|
|
||||||
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
v := Verifier{ed25519.PublicKey(user.PublicKey.PublicKey)}
|
||||||
valid := v.Verify(req.Method, date, req.Host, req.URL.Path, body, sigHeader)
|
valid := v.Verify(req.Method, req.URL, body, fedHeaders)
|
||||||
|
|
||||||
return valid, nil
|
return valid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractFederationHeaders(h http.Header) (time.Time, *SignatureHeader, error) {
|
|
||||||
gotDates := h.Values("date")
|
|
||||||
var date *Time
|
|
||||||
for i, raw := range gotDates {
|
|
||||||
if parsed, err := ParseTime(raw); err != nil {
|
|
||||||
log.Printf("invalid date[%d] header: %s", i, raw)
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
date = &parsed
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if date == nil {
|
|
||||||
return time.Time{}, nil, fmt.Errorf("missing date header")
|
|
||||||
}
|
|
||||||
|
|
||||||
gotSignature := h.Get("signature")
|
|
||||||
if gotSignature == "" {
|
|
||||||
return date.ToStd(), nil, fmt.Errorf("missing signature header")
|
|
||||||
}
|
|
||||||
sigHeader, err := ParseSignatureHeader(gotSignature)
|
|
||||||
if err != nil {
|
|
||||||
return date.ToStd(), nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return date.ToStd(), sigHeader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func hashSHA256(data []byte) []byte {
|
func hashSHA256(data []byte) []byte {
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
h.Write(data)
|
h.Write(data)
|
||||||
|
|
|
||||||
|
|
@ -4,97 +4,36 @@ import (
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFederationClient_ValidateSignatureHeader(t *testing.T) {
|
func TestFederationClient_ValidateSignatureHeader(t *testing.T) {
|
||||||
var (
|
var (
|
||||||
|
bobURL = &url.URL{Scheme: "https", Host: "bob.com"}
|
||||||
|
|
||||||
bobPrivBytes = must(base64.StdEncoding.DecodeString, "MC4CAQAwBQYDK2VwBCIEINOATgmaya61Ha9OEE+DD3RnOEqDaHyQ3yLf5upwskUU")
|
bobPrivBytes = must(base64.StdEncoding.DecodeString, "MC4CAQAwBQYDK2VwBCIEINOATgmaya61Ha9OEE+DD3RnOEqDaHyQ3yLf5upwskUU")
|
||||||
|
bobPriv = must(x509.ParsePKCS8PrivateKey, bobPrivBytes).(ed25519.PrivateKey)
|
||||||
|
signer = Signer{PrivateKey: bobPriv, UserURL: bobURL}
|
||||||
|
|
||||||
bobPubBytes = must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAQ08Z/FJ5f16o8mthLaFZMo4ssn0fJ7c+bipNYm3kId4=")
|
bobPubBytes = must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAQ08Z/FJ5f16o8mthLaFZMo4ssn0fJ7c+bipNYm3kId4=")
|
||||||
|
bobPub = must(x509.ParsePKIXPublicKey, bobPubBytes).(ed25519.PublicKey)
|
||||||
|
verifier = Verifier{PublicKey: bobPub}
|
||||||
|
|
||||||
|
method = "POST"
|
||||||
|
nonce = "myrandomnonce"
|
||||||
|
u = &url.URL{Scheme: "https", Host: "bob.com", Path: "/a/b/c", RawQuery: "z=foo&a=bar"}
|
||||||
|
body = []byte("hello")
|
||||||
)
|
)
|
||||||
|
|
||||||
bobPub := must(x509.ParsePKIXPublicKey, bobPubBytes).(ed25519.PublicKey)
|
toSign := NewSignatureData(method, nonce, u, hashSHA256(body))
|
||||||
bobPriv := must(x509.ParsePKCS8PrivateKey, bobPrivBytes).(ed25519.PrivateKey)
|
assert.Equal(t, `post /a/b/c?z=foo&a=bar myrandomnonce LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=`, toSign.String())
|
||||||
|
|
||||||
date := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
|
signed := signer.Sign(*toSign)
|
||||||
body := []byte("hello")
|
assert.Equal(t, true, verifier.Verify(method, u, body, signed), "signature verification failed")
|
||||||
|
|
||||||
sigData := NewSignatureData("POST", date, "example2.com", "/users/bob", hashSHA256(body))
|
assert.Equal(t, "myrandomnonce", signed.Nonce)
|
||||||
|
assert.Equal(t, bobURL, signed.SignedBy)
|
||||||
sig := Signer{PrivateKey: bobPriv, UserURL: &url.URL{Scheme: "https", Host: "example.com", Path: "/users/bob"}}.
|
assert.Equal(t, "datQHNaqJ1jeKzK3UeReUVf+B65JPq5P9LxfqUUJTMv3QNqDu5KawosKoduIRk4/D/A+EKjDhlcw0c7GzUlMCA==", base64.StdEncoding.EncodeToString(signed.Signature))
|
||||||
Sign(*sigData)
|
|
||||||
|
|
||||||
t.Run("validate against itself", func(t *testing.T) {
|
|
||||||
v := Verifier{
|
|
||||||
PublicKey: bobPub,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !v.Verify("POST", date, "example2.com", "/users/bob", body, sig) {
|
|
||||||
t.Error("signature verification failed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("validate against @lysand/api JS implementation", func(t *testing.T) {
|
|
||||||
expectedSignedString := `(request-target): post /users/bob
|
|
||||||
host: example2.com
|
|
||||||
date: 1970-01-01T00:00:00.000Z
|
|
||||||
digest: SHA-256=LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
|
|
||||||
`
|
|
||||||
assert.Equal(t, expectedSignedString, sigData.String())
|
|
||||||
|
|
||||||
expectedSignatureHeader := `keyId="https://example.com/users/bob",algorithm="ed25519",headers="(request-target) host date digest",signature="PbVicu1spnATYUznWn6N5ebNUC+w94U9k6y4dncLsr6hNfUD8CLInbUSkgR3AZrCWEZ+Md2+Lch70ofiSqXgAQ=="`
|
|
||||||
assert.Equal(t, expectedSignatureHeader, sig.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignatureInterop(t *testing.T) {
|
|
||||||
var (
|
|
||||||
bobPubBytes = must(base64.StdEncoding.DecodeString, "MCowBQYDK2VwAyEAgKNt+9eyOXdb7MSrrmHlsFD2H9NGwC+56PjpWD46Tcs=")
|
|
||||||
bobPrivBytes = must(base64.StdEncoding.DecodeString, "MC4CAQAwBQYDK2VwBCIEII+nkwT3nXwBp9FEE0q95RBBfikf6UTzPzdH2yrtIvL1")
|
|
||||||
)
|
|
||||||
|
|
||||||
bobPub := must(x509.ParsePKIXPublicKey, bobPubBytes).(ed25519.PublicKey)
|
|
||||||
bobPriv := must(x509.ParsePKCS8PrivateKey, bobPrivBytes).(ed25519.PrivateKey)
|
|
||||||
|
|
||||||
signedString := `(request-target): post /api/users/ec042557-8c30-492d-87d6-9e6495993072/inbox
|
|
||||||
host: lysand-test.i.devminer.xyz
|
|
||||||
date: 2024-07-25T21:03:24.866Z
|
|
||||||
digest: SHA-256=mPN5WKMoC4k3zor6FPTJUhDQ1JKX6zqA2QfEGh3omuc=
|
|
||||||
`
|
|
||||||
method := "POST"
|
|
||||||
dateHeader := "2024-07-25T21:03:24.866Z"
|
|
||||||
date := must(ParseTime, dateHeader)
|
|
||||||
host := "lysand-test.i.devminer.xyz"
|
|
||||||
path := "/api/users/ec042557-8c30-492d-87d6-9e6495993072/inbox"
|
|
||||||
body := []byte(`{"type":"Follow","id":"2265b3b2-a176-4b20-8fcf-ac82cf2efd7d","author":"https://lysand.i.devminer.xyz/users/0190d697-c83a-7376-8d15-0f77fd09e180","followee":"https://lysand-test.i.devminer.xyz/api/users/ec042557-8c30-492d-87d6-9e6495993072/","created_at":"2024-07-25T21:03:24.863Z","uri":"https://lysand.i.devminer.xyz/follows/2265b3b2-a176-4b20-8fcf-ac82cf2efd7d"}`)
|
|
||||||
signatureHeader := `keyId="https://lysand.i.devminer.xyz/users/0190d697-c83a-7376-8d15-0f77fd09e180",algorithm="ed25519",headers="(request-target) host date digest",signature="KUkKYexLk2hOfE+NVIacLDHSJP2QpX4xJGclHhQIM39ce2or7UJauRtCL8eWrhpSgQdVPk11bYhvvi8fdCruBw=="`
|
|
||||||
|
|
||||||
sigData := NewSignatureData(method, date.ToStd(), host, path, hashSHA256(body))
|
|
||||||
assert.Equal(t, signedString, sigData.String())
|
|
||||||
|
|
||||||
t.Run("signature header parsing", func(t *testing.T) {
|
|
||||||
parsedSignatureHeader, err := ParseSignatureHeader(signatureHeader)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
assert.Equal(t, "https://lysand.i.devminer.xyz/users/0190d697-c83a-7376-8d15-0f77fd09e180", parsedSignatureHeader.KeyID.String())
|
|
||||||
assert.Equal(t, "ed25519", parsedSignatureHeader.Algorithm)
|
|
||||||
assert.Equal(t, "(request-target) host date digest", parsedSignatureHeader.Headers)
|
|
||||||
assert.Equal(t, sigData.Sign(bobPriv), parsedSignatureHeader.Signature)
|
|
||||||
|
|
||||||
v := Verifier{PublicKey: bobPub}
|
|
||||||
if !v.Verify(method, date.ToStd(), host, path, body, parsedSignatureHeader) {
|
|
||||||
t.Error("signature verification failed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("signature header generation", func(t *testing.T) {
|
|
||||||
sig := Signer{PrivateKey: bobPriv, UserURL: &url.URL{Scheme: "https", Host: "lysand.i.devminer.xyz", Path: "/users/0190d697-c83a-7376-8d15-0f77fd09e180"}}.
|
|
||||||
Sign(*sigData)
|
|
||||||
assert.Equal(t, signatureHeader, sig.String())
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
54
pkg/lysand/federation_headers.go
Normal file
54
pkg/lysand/federation_headers.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package lysand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FederationHeaders represents the signature header of the Lysand protocol. For more information, see the [Spec].
|
||||||
|
//
|
||||||
|
// [Spec]: https://versia.pub/signatures#signature-definition
|
||||||
|
type FederationHeaders struct {
|
||||||
|
// SignedBy is the URL to a user
|
||||||
|
SignedBy *url.URL
|
||||||
|
// Nonce is a random string, used to prevent replay attacks
|
||||||
|
Nonce string
|
||||||
|
// Signature is the signature of the request
|
||||||
|
Signature []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FederationHeaders) Inject(h http.Header) {
|
||||||
|
h.Set("x-signed-by", f.SignedBy.String())
|
||||||
|
h.Set("x-nonce", f.Nonce)
|
||||||
|
h.Set("x-signature", base64.StdEncoding.EncodeToString(f.Signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractFederationHeaders(h http.Header) (*FederationHeaders, error) {
|
||||||
|
signedBy := h.Get("x-signed-by")
|
||||||
|
if signedBy == "" {
|
||||||
|
return nil, fmt.Errorf("missing x-signed-by header")
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse(signedBy)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := h.Get("x-nonce")
|
||||||
|
if nonce == "" {
|
||||||
|
return nil, fmt.Errorf("missing x-nonce header")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := h.Get("x-signature")
|
||||||
|
if signature == "" {
|
||||||
|
return nil, fmt.Errorf("missing x-signature header")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &FederationHeaders{
|
||||||
|
SignedBy: u,
|
||||||
|
Nonce: nonce,
|
||||||
|
Signature: []byte(signature),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
18
pkg/lysand/federation_headers_test.go
Normal file
18
pkg/lysand/federation_headers_test.go
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package lysand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFederationHeaders_String(t *testing.T) {
|
||||||
|
one := SignatureData{
|
||||||
|
RequestMethod: "POST",
|
||||||
|
Nonce: "1234567890",
|
||||||
|
URL: &url.URL{Scheme: "https", Host: "bob.com", Path: "/users/bob", RawQuery: "z=foo&a=bar"},
|
||||||
|
Digest: hashSHA256([]byte("hello")),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, "post /users/bob?z=foo&a=bar 1234567890 LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=", one.String())
|
||||||
|
}
|
||||||
|
|
@ -1,70 +0,0 @@
|
||||||
package lysand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type SignatureData struct {
|
|
||||||
RequestMethod string
|
|
||||||
Date time.Time
|
|
||||||
Host string
|
|
||||||
Path string
|
|
||||||
Digest []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSignatureData(method string, date time.Time, host, path string, digest []byte) *SignatureData {
|
|
||||||
return &SignatureData{
|
|
||||||
RequestMethod: method,
|
|
||||||
Date: date,
|
|
||||||
Host: host,
|
|
||||||
Path: path,
|
|
||||||
Digest: digest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SignatureData) String() string {
|
|
||||||
return strings.Join([]string{
|
|
||||||
fmt.Sprintf("(request-target): %s %s", strings.ToLower(s.RequestMethod), s.Path),
|
|
||||||
fmt.Sprintf("host: %s", s.Host),
|
|
||||||
fmt.Sprintf("date: %s", TimeFromStd(s.Date).String()),
|
|
||||||
fmt.Sprintf("digest: SHA-256=%s", base64.StdEncoding.EncodeToString(s.Digest)),
|
|
||||||
"",
|
|
||||||
}, "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SignatureData) Validate(pubKey ed25519.PublicKey, signature []byte) bool {
|
|
||||||
return ed25519.Verify(pubKey, []byte(s.String()), signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SignatureData) Sign(privKey ed25519.PrivateKey) []byte {
|
|
||||||
return ed25519.Sign(privKey, []byte(s.String()))
|
|
||||||
}
|
|
||||||
|
|
||||||
type Signer struct {
|
|
||||||
PrivateKey ed25519.PrivateKey
|
|
||||||
UserURL *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s Signer) Sign(signatureData SignatureData) *SignatureHeader {
|
|
||||||
return &SignatureHeader{
|
|
||||||
KeyID: s.UserURL,
|
|
||||||
Algorithm: "ed25519",
|
|
||||||
Headers: "(request-target) host date digest",
|
|
||||||
Signature: signatureData.Sign(s.PrivateKey),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Verifier struct {
|
|
||||||
PublicKey ed25519.PublicKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v Verifier) Verify(method string, date time.Time, host, path string, body []byte, sigHeader *SignatureHeader) bool {
|
|
||||||
sigData := NewSignatureData(method, date, host, path, hashSHA256(body))
|
|
||||||
|
|
||||||
return sigData.Validate(v.PublicKey, sigHeader.Signature)
|
|
||||||
}
|
|
||||||
64
pkg/lysand/signature_data.go
Normal file
64
pkg/lysand/signature_data.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package lysand
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SignatureData struct {
|
||||||
|
// RequestMethod is the *lowercase* HTTP method of the request
|
||||||
|
RequestMethod string
|
||||||
|
// Nonce is a random byte array, used to prevent replay attacks
|
||||||
|
Nonce string
|
||||||
|
// RawPath is the path of the request, without the query string
|
||||||
|
URL *url.URL
|
||||||
|
// Digest is the SHA-256 hash of the request body
|
||||||
|
Digest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSignatureData(method, nonce string, u *url.URL, digest []byte) *SignatureData {
|
||||||
|
return &SignatureData{
|
||||||
|
RequestMethod: method,
|
||||||
|
Nonce: nonce,
|
||||||
|
URL: u,
|
||||||
|
Digest: digest,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureData) String() string {
|
||||||
|
return fmt.Sprintf("%s %s?%s %s %s", strings.ToLower(s.RequestMethod), s.URL.Path, s.URL.RawQuery, s.Nonce, base64.StdEncoding.EncodeToString(s.Digest))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureData) Validate(pubKey ed25519.PublicKey, signature []byte) bool {
|
||||||
|
return ed25519.Verify(pubKey, []byte(s.String()), signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SignatureData) Sign(privKey ed25519.PrivateKey) []byte {
|
||||||
|
return ed25519.Sign(privKey, []byte(s.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Signer struct {
|
||||||
|
PrivateKey ed25519.PrivateKey
|
||||||
|
UserURL *url.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Signer) Sign(signatureData SignatureData) *FederationHeaders {
|
||||||
|
return &FederationHeaders{
|
||||||
|
SignedBy: s.UserURL,
|
||||||
|
Nonce: signatureData.Nonce,
|
||||||
|
Signature: signatureData.Sign(s.PrivateKey),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Verifier struct {
|
||||||
|
PublicKey ed25519.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v Verifier) Verify(method string, u *url.URL, body []byte, fedHeaders *FederationHeaders) bool {
|
||||||
|
sigData := NewSignatureData(method, fedHeaders.Nonce, u, hashSHA256(body))
|
||||||
|
|
||||||
|
return sigData.Validate(v.PublicKey, fedHeaders.Signature)
|
||||||
|
}
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package lysand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidSignatureHeader = errors.New("invalid signature header")
|
|
||||||
)
|
|
||||||
|
|
||||||
type SignatureHeader struct {
|
|
||||||
// URL to a user
|
|
||||||
KeyID *url.URL
|
|
||||||
Headers string
|
|
||||||
Algorithm string
|
|
||||||
Signature []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s SignatureHeader) String() string {
|
|
||||||
return strings.Join([]string{
|
|
||||||
fmt.Sprintf(`keyId="%s"`, s.KeyID.String()),
|
|
||||||
fmt.Sprintf(`algorithm="%s"`, s.Algorithm),
|
|
||||||
fmt.Sprintf(`headers="%s"`, s.Headers),
|
|
||||||
fmt.Sprintf(`signature="%s"`, base64.StdEncoding.EncodeToString(s.Signature)),
|
|
||||||
}, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseSignatureHeader parses strings in the form of
|
|
||||||
// `keyId="<URL>",algorithm="ed25519",headers="(request-target) host date digest",signature="<BASE64 SIGNATURE>"`
|
|
||||||
func ParseSignatureHeader(raw string) (*SignatureHeader, error) {
|
|
||||||
parts := strings.Split(raw, ",")
|
|
||||||
if len(parts) != 4 {
|
|
||||||
return nil, ErrInvalidSignatureHeader
|
|
||||||
}
|
|
||||||
|
|
||||||
sig := &SignatureHeader{}
|
|
||||||
|
|
||||||
for _, part := range parts {
|
|
||||||
kv := strings.SplitN(part, "=", 2)
|
|
||||||
kv[1] = strings.TrimPrefix(kv[1], "\"")
|
|
||||||
kv[1] = strings.TrimSuffix(kv[1], "\"")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
switch kv[0] {
|
|
||||||
case "keyId":
|
|
||||||
sig.KeyID, err = url.Parse(kv[1])
|
|
||||||
case "algorithm":
|
|
||||||
sig.Algorithm = kv[1]
|
|
||||||
case "headers":
|
|
||||||
sig.Headers = kv[1]
|
|
||||||
case "signature":
|
|
||||||
sig.Signature, err = base64.StdEncoding.DecodeString(kv[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sig, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
package lysand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParseSignatureHeader(t *testing.T) {
|
|
||||||
data := `keyId="https://example.com/users/bob",algorithm="ed25519",headers="(request-target) host date digest",signature="PbVicu1spnATYUznWn6N5ebNUC+w94U9k6y4dncLsr6hNfUD8CLInbUSkgR3AZrCWEZ+Md2+Lch70ofiSqXgAQ=="`
|
|
||||||
expectedSignature := must(base64.StdEncoding.DecodeString, "PbVicu1spnATYUznWn6N5ebNUC+w94U9k6y4dncLsr6hNfUD8CLInbUSkgR3AZrCWEZ+Md2+Lch70ofiSqXgAQ==")
|
|
||||||
|
|
||||||
sig, err := ParseSignatureHeader(data)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "https://example.com/users/bob", sig.KeyID.String())
|
|
||||||
assert.Equal(t, "ed25519", sig.Algorithm)
|
|
||||||
assert.Equal(t, "(request-target) host date digest", sig.Headers)
|
|
||||||
assert.Equal(t, expectedSignature, sig.Signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSignatureHeader_String(t *testing.T) {
|
|
||||||
one := SignatureData{
|
|
||||||
RequestMethod: "POST",
|
|
||||||
Date: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
||||||
Host: "example2.com",
|
|
||||||
Path: "/users/bob",
|
|
||||||
Digest: hashSHA256([]byte("hello")),
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := `(request-target): post /users/bob
|
|
||||||
host: example2.com
|
|
||||||
date: 1970-01-01T00:00:00.000Z
|
|
||||||
digest: SHA-256=LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=
|
|
||||||
`
|
|
||||||
|
|
||||||
assert.Equal(t, expected, one.String())
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue