Implemented API endpoint to sign arbitrary data

This commit is contained in:
ErickSkrauch
2024-03-05 13:07:54 +01:00
parent f5bc474b4d
commit 436ff7c294
20 changed files with 748 additions and 255 deletions

View File

@@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"slices"
"strings"
"time"
@@ -15,14 +16,23 @@ import (
var now = time.Now
var signingMethod = jwt.SigningMethodHS256
const scopesClaim = "scopes"
type Scope string
const (
ProfileScope Scope = "profiles"
ProfilesScope Scope = "profiles"
SignScope Scope = "sign"
)
var validScopes = []Scope{
ProfilesScope,
SignScope,
}
type claims struct {
jwt.RegisteredClaims
Scopes []Scope `json:"scopes"`
}
func NewJwt(key []byte) *Jwt {
return &Jwt{
Key: key,
@@ -38,11 +48,20 @@ func (t *Jwt) NewToken(scopes ...Scope) (string, error) {
return "", errors.New("you must specify at least one scope")
}
token := jwt.NewWithClaims(signingMethod, jwt.MapClaims{
"iss": "chrly",
"iat": now().Unix(),
scopesClaim: scopes,
})
for _, scope := range scopes {
if !slices.Contains(validScopes, scope) {
return "", fmt.Errorf("unknown scope %s", scope)
}
}
token := jwt.New(signingMethod)
token.Claims = &claims{
jwt.RegisteredClaims{
Issuer: "chrly",
IssuedAt: jwt.NewNumericDate(now()),
},
scopes,
}
token.Header["v"] = version.MajorVersion
return token.SignedString(t.Key)
@@ -52,7 +71,7 @@ func (t *Jwt) NewToken(scopes ...Scope) (string, error) {
var MissingAuthenticationError = errors.New("authentication value not provided")
var InvalidTokenError = errors.New("passed authentication value is invalid")
func (t *Jwt) Authenticate(req *http.Request) error {
func (t *Jwt) Authenticate(req *http.Request, scope Scope) error {
bearerToken := req.Header.Get("Authorization")
if bearerToken == "" {
return MissingAuthenticationError
@@ -62,8 +81,8 @@ func (t *Jwt) Authenticate(req *http.Request) error {
return InvalidTokenError
}
tokenStr := bearerToken[7:]
token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
tokenStr := bearerToken[7:] // trim "bearer " part
token, err := jwt.ParseWithClaims(tokenStr, &claims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
@@ -78,5 +97,10 @@ func (t *Jwt) Authenticate(req *http.Request) error {
return errors.Join(InvalidTokenError, errors.New("missing v header"))
}
claims := token.Claims.(*claims)
if !slices.Contains(claims.Scopes, scope) {
return errors.New("the token doesn't have the scope to perform the action")
}
return nil
}

View File

@@ -16,10 +16,16 @@ func TestJwtAuth_NewToken(t *testing.T) {
return time.Date(2024, 2, 1, 11, 26, 15, 0, time.UTC)
}
t.Run("with scope", func(t *testing.T) {
token, err := jwt.NewToken(ProfileScope, "custom-scope")
t.Run("with known scope", func(t *testing.T) {
token, err := jwt.NewToken(ProfilesScope, SignScope)
require.NoError(t, err)
require.Equal(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInYiOjV9.eyJpYXQiOjE3MDY3ODY3NzUsImlzcyI6ImNocmx5Iiwic2NvcGVzIjpbInByb2ZpbGVzIiwiY3VzdG9tLXNjb3BlIl19.Iq673YyWWkJZjIkBmKYRN8Lx9qoD39S_e-MegG0aORM", token)
require.Equal(t, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsInYiOjV9.eyJpc3MiOiJjaHJseSIsImlhdCI6MTcwNjc4Njc3NSwic2NvcGVzIjpbInByb2ZpbGVzIiwic2lnbiJdfQ.HkNGiDba3I_bLGN6sF0eTE5n6rMLgYfAZZEqI4xb2X4", token)
})
t.Run("with unknown scope", func(t *testing.T) {
token, err := jwt.NewToken("scope-123")
require.ErrorContains(t, err, "unknown")
require.Empty(t, token)
})
t.Run("no scopes", func(t *testing.T) {
@@ -34,41 +40,48 @@ func TestJwtAuth_Authenticate(t *testing.T) {
t.Run("success", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "Bearer "+jwtString)
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.NoError(t, err)
})
t.Run("has no required scope", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "Bearer "+jwtString)
err := jwt.Authenticate(req, SignScope)
require.ErrorContains(t, err, "scope")
})
t.Run("request without auth header", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.ErrorIs(t, err, MissingAuthenticationError)
})
t.Run("no bearer token prefix", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "trash")
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.ErrorIs(t, err, InvalidTokenError)
})
t.Run("bearer token but not jwt", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "Bearer seems.like.jwt")
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.ErrorIs(t, err, InvalidTokenError)
})
t.Run("invalid signature", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "Bearer "+jwtString+"123")
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.ErrorIs(t, err, InvalidTokenError)
})
t.Run("missing v header", func(t *testing.T) {
req := httptest.NewRequest("POST", "http://localhost", nil)
req.Header.Add("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE3MDY3ODY3NzUsImlzcyI6ImNocmx5Iiwic2NvcGVzIjpbInByb2ZpbGVzIl19.zOX2ZKyU37kjwt1p9uCHxALxWQD2UC0wWcAcNvBXGq0")
err := jwt.Authenticate(req)
err := jwt.Authenticate(req, ProfilesScope)
require.ErrorIs(t, err, InvalidTokenError)
require.ErrorContains(t, err, "missing v header")
})

View File

@@ -1,15 +1,18 @@
package security
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"encoding/base64"
"crypto/x509"
"encoding/pem"
"errors"
"io"
)
var randomReader = rand.Reader
var invalidKeyFormat = errors.New(`invalid key format: it should be"der" or "pem"`)
func NewSigner(key *rsa.PrivateKey) *Signer {
return &Signer{Key: key}
@@ -19,23 +22,38 @@ type Signer struct {
Key *rsa.PrivateKey
}
func (s *Signer) SignTextures(ctx context.Context, textures string) (string, error) {
message := []byte(textures)
func (s *Signer) Sign(data io.Reader) ([]byte, error) {
messageHash := sha1.New()
_, err := messageHash.Write(message)
_, err := io.Copy(messageHash, data)
if err != nil {
return "", err
return nil, err
}
messageHashSum := messageHash.Sum(nil)
signature, err := rsa.SignPKCS1v15(randomReader, s.Key, crypto.SHA1, messageHashSum)
if err != nil {
return "", err
return nil, err
}
return base64.StdEncoding.EncodeToString(signature), nil
return signature, nil
}
func (s *Signer) GetPublicKey(ctx context.Context) (*rsa.PublicKey, error) {
return &s.Key.PublicKey, nil
func (s *Signer) GetPublicKey(format string) ([]byte, error) {
if format != "der" && format != "pem" {
return nil, invalidKeyFormat
}
asn1Bytes, err := x509.MarshalPKIXPublicKey(s.Key.Public())
if err != nil {
return nil, err
}
if format == "pem" {
return pem.EncodeToMemory(&pem.Block{
Type: "PUBLIC KEY",
Bytes: asn1Bytes,
}), nil
}
return asn1Bytes, nil
}

View File

@@ -1,10 +1,9 @@
package security
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"strings"
"testing"
"github.com/stretchr/testify/require"
@@ -17,7 +16,7 @@ func (c *ConstantReader) Read(p []byte) (int, error) {
return 1, nil
}
func TestSigner_SignTextures(t *testing.T) {
func TestSigner_Sign(t *testing.T) {
randomReader = &ConstantReader{}
rawKey, _ := pem.Decode([]byte("-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBANbUpVCZkMKpfvYZ08W3lumdAaYxLBnmUDlzHBQH3DpYef5WCO32\nTDU6feIJ58A0lAywgtZ4wwi2dGHOz/1hAvcCAwEAAQJAItaxSHTe6PKbyEU/9pxj\nONdhYRYwVLLo56gnMYhkyoEqaaMsfov8hhoepkYZBMvZFB2bDOsQ2SaJ+E2eiBO4\nAQIhAPssS0+BR9w0bOdmjGqmdE9NrN5UJQcOW13s29+6QzUBAiEA2vWOepA5Apiu\npEA3pwoGdkVCrNSnnKjDQzDXBnpd3/cCIEFNd9sY4qUG4FWdXN6RnmXL7Sj0uZfH\nDMwzu8rEM5sBAiEAhvdoDNqLmbMdq3c+FsPSOeL1d21Zp/JK8kbPtFmHNf8CIQDV\n6FSZDwvWfuxaM7BsycQONkjDBTPNu+lqctJBGnBv3A==\n-----END RSA PRIVATE KEY-----\n"))
@@ -25,9 +24,14 @@ func TestSigner_SignTextures(t *testing.T) {
signer := NewSigner(key)
signature, err := signer.SignTextures(context.Background(), "eyJ0aW1lc3RhbXAiOjE2MTQzMDcxMzQsInByb2ZpbGVJZCI6ImZmYzhmZGM5NTgyNDUwOWU4YTU3Yzk5Yjk0MGZiOTk2IiwicHJvZmlsZU5hbWUiOiJFcmlja1NrcmF1Y2giLCJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly9lbHkuYnkvc3RvcmFnZS9za2lucy82OWM2NzQwZDI5OTNlNWQ2ZjZhN2ZjOTI0MjBlZmMyOS5wbmcifX0sImVseSI6dHJ1ZX0")
signature, err := signer.Sign(strings.NewReader("mock string to sign"))
require.NoError(t, err)
require.Equal(t, "IyHCxTP5ITquEXTHcwCtLd08jWWy16JwlQeWg8naxhoAVQecHGRdzHRscuxtdq/446kmeox7h4EfRN2A2ZLL+A==", signature)
require.Equal(t, []byte{
0xd0, 0x88, 0xc6, 0x65, 0x27, 0x5d, 0xe4, 0x86, 0x6b, 0x7a, 0x5a, 0xd, 0x94, 0x6f, 0x80, 0x88, 0x12, 0x8e, 0x65,
0x75, 0xfb, 0xba, 0xcb, 0x7f, 0x90, 0xf5, 0xae, 0x5d, 0x2c, 0x5d, 0x60, 0xf6, 0x83, 0x54, 0xd3, 0x40, 0xd, 0x1f,
0xc0, 0xbc, 0x6d, 0xa8, 0x6f, 0x6, 0xd8, 0x38, 0x74, 0x5b, 0x4f, 0x15, 0x82, 0x6d, 0x67, 0x95, 0x7b, 0xf, 0xcc,
0xf3, 0x51, 0xfe, 0xcd, 0xb9, 0x1e, 0xdf,
}, signature)
}
func TestSigner_GetPublicKey(t *testing.T) {
@@ -38,7 +42,40 @@ func TestSigner_GetPublicKey(t *testing.T) {
signer := NewSigner(key)
publicKey, err := signer.GetPublicKey(context.Background())
require.NoError(t, err)
require.IsType(t, &rsa.PublicKey{}, publicKey)
t.Run("pem format", func(t *testing.T) {
publicKey, err := signer.GetPublicKey("pem")
require.NoError(t, err)
require.Equal(t, []byte{
0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0x42, 0x45, 0x47, 0x49, 0x4e, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20,
0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d, 0xa, 0x4d, 0x46, 0x77, 0x77, 0x44, 0x51, 0x59, 0x4a, 0x4b,
0x6f, 0x5a, 0x49, 0x68, 0x76, 0x63, 0x4e, 0x41, 0x51, 0x45, 0x42, 0x42, 0x51, 0x41, 0x44, 0x53, 0x77, 0x41,
0x77, 0x53, 0x41, 0x4a, 0x42, 0x41, 0x4e, 0x62, 0x55, 0x70, 0x56, 0x43, 0x5a, 0x6b, 0x4d, 0x4b, 0x70, 0x66,
0x76, 0x59, 0x5a, 0x30, 0x38, 0x57, 0x33, 0x6c, 0x75, 0x6d, 0x64, 0x41, 0x61, 0x59, 0x78, 0x4c, 0x42, 0x6e,
0x6d, 0xa, 0x55, 0x44, 0x6c, 0x7a, 0x48, 0x42, 0x51, 0x48, 0x33, 0x44, 0x70, 0x59, 0x65, 0x66, 0x35, 0x57,
0x43, 0x4f, 0x33, 0x32, 0x54, 0x44, 0x55, 0x36, 0x66, 0x65, 0x49, 0x4a, 0x35, 0x38, 0x41, 0x30, 0x6c, 0x41,
0x79, 0x77, 0x67, 0x74, 0x5a, 0x34, 0x77, 0x77, 0x69, 0x32, 0x64, 0x47, 0x48, 0x4f, 0x7a, 0x2f, 0x31, 0x68,
0x41, 0x76, 0x63, 0x43, 0x41, 0x77, 0x45, 0x41, 0x41, 0x51, 0x3d, 0x3d, 0xa, 0x2d, 0x2d, 0x2d, 0x2d, 0x2d,
0x45, 0x4e, 0x44, 0x20, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x43, 0x20, 0x4b, 0x45, 0x59, 0x2d, 0x2d, 0x2d, 0x2d,
0x2d, 0xa,
}, publicKey)
})
t.Run("der format", func(t *testing.T) {
publicKey, err := signer.GetPublicKey("der")
require.NoError(t, err)
require.Equal(t, []byte{
0x30, 0x5c, 0x30, 0xd, 0x6, 0x9, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0xd, 0x1, 0x1, 0x1, 0x5, 0x0, 0x3, 0x4b, 0x0,
0x30, 0x48, 0x2, 0x41, 0x0, 0xd6, 0xd4, 0xa5, 0x50, 0x99, 0x90, 0xc2, 0xa9, 0x7e, 0xf6, 0x19, 0xd3, 0xc5,
0xb7, 0x96, 0xe9, 0x9d, 0x1, 0xa6, 0x31, 0x2c, 0x19, 0xe6, 0x50, 0x39, 0x73, 0x1c, 0x14, 0x7, 0xdc, 0x3a,
0x58, 0x79, 0xfe, 0x56, 0x8, 0xed, 0xf6, 0x4c, 0x35, 0x3a, 0x7d, 0xe2, 0x9, 0xe7, 0xc0, 0x34, 0x94, 0xc,
0xb0, 0x82, 0xd6, 0x78, 0xc3, 0x8, 0xb6, 0x74, 0x61, 0xce, 0xcf, 0xfd, 0x61, 0x2, 0xf7, 0x2, 0x3, 0x1, 0x0,
0x1,
}, publicKey)
})
t.Run("unknown format", func(t *testing.T) {
publicKey, err := signer.GetPublicKey("unknown")
require.Nil(t, publicKey)
require.ErrorContains(t, err, "invalid")
})
}