diff --git a/protocol/meshcore/crypto/jwt/go.mod b/protocol/meshcore/crypto/jwt/go.mod new file mode 100644 index 0000000..d84c7f1 --- /dev/null +++ b/protocol/meshcore/crypto/jwt/go.mod @@ -0,0 +1,12 @@ +module git.maze.io/go/ham/protocol/meshcore/crypto/jwt + +go 1.25.6 + +replace git.maze.io/go/ham => ../../../.. + +require ( + git.maze.io/go/ham v0.0.0-20260214145931-f1ecbfaf8df0 + github.com/golang-jwt/jwt/v5 v5.3.1 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/protocol/meshcore/crypto/jwt/go.sum b/protocol/meshcore/crypto/jwt/go.sum new file mode 100644 index 0000000..f276846 --- /dev/null +++ b/protocol/meshcore/crypto/jwt/go.sum @@ -0,0 +1,4 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= diff --git a/protocol/meshcore/crypto/jwt/jwt.go b/protocol/meshcore/crypto/jwt/jwt.go new file mode 100644 index 0000000..bce520d --- /dev/null +++ b/protocol/meshcore/crypto/jwt/jwt.go @@ -0,0 +1,78 @@ +// Package jwt implements JSON Web Tokens (JWT) using Meshcore Ed25519 keys +package jwt + +import ( + "fmt" + + "git.maze.io/go/ham/protocol/meshcore/crypto" + + "github.com/golang-jwt/jwt/v5" +) + +var SigningMethod jwt.SigningMethod + +func init() { + SigningMethod = new(SigningMethodEd25519) +} + +type SigningMethodEd25519 struct{} + +func (m *SigningMethodEd25519) Alg() string { + return "Ed25519" +} + +func (m *SigningMethodEd25519) Sign(signingString string, key any) ([]byte, error) { + var ( + privateKey *crypto.PrivateKey + err error + ) + switch key := key.(type) { + case *crypto.PrivateKey: + privateKey = key + case []byte: + switch len(key) { + case crypto.SeedSize: + if privateKey, err = crypto.NewPrivateKeyFromSeed(key); err != nil { + return nil, err + } + default: + if privateKey, err = crypto.NewPrivateKey(key); err != nil { + return nil, err + } + } + default: + return nil, fmt.Errorf("jwt: invalid Ed25519 private key %T", key) + } + + return crypto.Sign(privateKey, []byte(signingString)), nil +} + +func (m *SigningMethodEd25519) Verify(signingString string, sig []byte, key any) error { + var ( + publicKey *crypto.PublicKey + err error + ) + switch key := key.(type) { + case *crypto.PublicKey: + publicKey = key + case crypto.PublicKey: + publicKey = &key + case *crypto.PrivateKey: + if publicKey, err = crypto.NewPublicKey(key.PublicKey()); err != nil { + return err + } + case []byte: + if publicKey, err = crypto.NewPublicKey(key); err != nil { + return err + } + case string: + if publicKey, err = crypto.DecodePublicKey(key); err != nil { + return err + } + default: + return fmt.Errorf("jwt: invalid Ed25519 public key %T", key) + } + return crypto.Verify(publicKey, []byte(signingString), sig) +} + +var _ jwt.SigningMethod = (*SigningMethodEd25519)(nil) diff --git a/protocol/meshcore/crypto/jwt/jwt_test.go b/protocol/meshcore/crypto/jwt/jwt_test.go new file mode 100644 index 0000000..35ff0bd --- /dev/null +++ b/protocol/meshcore/crypto/jwt/jwt_test.go @@ -0,0 +1,60 @@ +package jwt + +import ( + "testing" + + "github.com/golang-jwt/jwt/v5" +) + +func TestToken_SigningString(t1 *testing.T) { + type fields struct { + Raw string + Method jwt.SigningMethod + Header map[string]any + Claims jwt.Claims + Signature []byte + Valid bool + } + tests := []struct { + name string + fields fields + want string + wantErr bool + }{ + { + name: "", + fields: fields{ + Raw: "", + Method: SigningMethod, + Header: map[string]any{ + "typ": "JWT", + "alg": SigningMethod.Alg(), + }, + Claims: jwt.RegisteredClaims{}, + Valid: false, + }, + want: "eyJhbGciOiJFZDI1NTE5IiwidHlwIjoiSldUIn0.e30", + wantErr: false, + }, + } + for _, tt := range tests { + t1.Run(tt.name, func(t1 *testing.T) { + t := &jwt.Token{ + Raw: tt.fields.Raw, + Method: tt.fields.Method, + Header: tt.fields.Header, + Claims: tt.fields.Claims, + Signature: tt.fields.Signature, + Valid: tt.fields.Valid, + } + got, err := t.SigningString() + if (err != nil) != tt.wantErr { + t1.Errorf("SigningString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t1.Errorf("SigningString() got = %v, want %v", got, tt.want) + } + }) + } +}