diff --git a/cmd/jwt/app.go b/cmd/jwt/app.go index b364927..c8fd936 100644 --- a/cmd/jwt/app.go +++ b/cmd/jwt/app.go @@ -136,6 +136,8 @@ func verifyToken() error { return jwt.ParseECPublicKeyFromPEM(data) } else if isRs() { return jwt.ParseRSAPublicKeyFromPEM(data) + } else if isEd() { + return jwt.ParseEdPublicKeyFromPEM(data) } return data, nil }) @@ -229,6 +231,15 @@ func signToken() error { return err } } + } else if isEd() { + if k, ok := key.([]byte); !ok { + return fmt.Errorf("Couldn't convert key data to key") + } else { + key, err = jwt.ParseEdPrivateKeyFromPEM(k) + if err != nil { + return err + } + } } if out, err := token.SignedString(key); err == nil { @@ -280,3 +291,7 @@ func isEs() bool { func isRs() bool { return strings.HasPrefix(*flagAlg, "RS") || strings.HasPrefix(*flagAlg, "PS") } + +func isEd() bool { + return strings.HasPrefix(strings.ToUpper(*flagAlg), "Ed") +} diff --git a/ed25519.go b/ed25519.go new file mode 100644 index 0000000..a2f8ddb --- /dev/null +++ b/ed25519.go @@ -0,0 +1,81 @@ +package jwt + +import ( + "errors" + + "crypto/ed25519" +) + +var ( + ErrEd25519Verification = errors.New("ed25519: verification error") +) + +// Implements the EdDSA family +// Expects ed25519.PrivateKey for signing and ed25519.PublicKey for verification +type SigningMethodEd25519 struct{} + +// Specific instance for EdDSA +var ( + SigningMethodEdDSA *SigningMethodEd25519 +) + +func init() { + SigningMethodEdDSA = &SigningMethodEd25519{} + RegisterSigningMethod(SigningMethodEdDSA.Alg(), func() SigningMethod { + return SigningMethodEdDSA + }) +} + +func (m *SigningMethodEd25519) Alg() string { + return "EdDSA" +} + +// Implements the Verify method from SigningMethod +// For this verify method, key must be an ed25519.PublicKey +func (m *SigningMethodEd25519) Verify(signingString, signature string, key interface{}) error { + var err error + var ed25519Key ed25519.PublicKey + var ok bool + + if ed25519Key, ok = key.(ed25519.PublicKey); !ok { + return ErrInvalidKeyType + } + + if len(ed25519Key) != ed25519.PublicKeySize { + return ErrInvalidKey + } + + // Decode the signature + var sig []byte + if sig, err = DecodeSegment(signature); err != nil { + return err + } + + // Verify the signature + if !ed25519.Verify(ed25519Key, []byte(signingString), sig) { + return ErrEd25519Verification + } + + return nil +} + +// Implements the Sign method from SigningMethod +// For this signing method, key must be an ed25519.PrivateKey +func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (string, error) { + var ed25519Key ed25519.PrivateKey + var ok bool + + if ed25519Key, ok = key.(ed25519.PrivateKey); !ok { + return "", ErrInvalidKeyType + } + + // ed25519.Sign panics if private key not equal to ed25519.PrivateKeySize + // this allows to avoid recover usage + if len(ed25519Key) != ed25519.PrivateKeySize { + return "", ErrInvalidKey + } + + // Sign the string and return the encoded result + sig := ed25519.Sign(ed25519Key, []byte(signingString)) + return EncodeSegment(sig), nil +} diff --git a/ed25519_test.go b/ed25519_test.go new file mode 100644 index 0000000..26177e2 --- /dev/null +++ b/ed25519_test.go @@ -0,0 +1,84 @@ +package jwt_test + +import ( + "io/ioutil" + "strings" + "testing" + + "github.com/golang-jwt/jwt" +) + +var ed25519TestData = []struct { + name string + keys map[string]string + tokenString string + alg string + claims map[string]interface{} + valid bool +}{ + { + "Basic Ed25519", + map[string]string{"private": "test/ed25519-private.pem", "public": "test/ed25519-public.pem"}, + "eyJhbGciOiJFRDI1NTE5IiwidHlwIjoiSldUIn0.eyJmb28iOiJiYXIifQ.ESuVzZq1cECrt9Od_gLPVG-_6uRP_8Nq-ajx6CtmlDqRJZqdejro2ilkqaQgSL-siE_3JMTUW7UwAorLaTyFCw", + "EdDSA", + map[string]interface{}{"foo": "bar"}, + true, + }, + { + "Basic Ed25519", + map[string]string{"private": "test/ed25519-private.pem", "public": "test/ed25519-public.pem"}, + "eyJhbGciOiJFRDI1NTE5IiwidHlwIjoiSldUIn0.eyJmb28iOiJiYXoifQ.ESuVzZq1cECrt9Od_gLPVG-_6uRP_8Nq-ajx6CtmlDqRJZqdejro2ilkqaQgSL-siE_3JMTUW7UwAorLaTyFCw", + "EdDSA", + map[string]interface{}{"foo": "bar"}, + false, + }, +} + +func TestEd25519Verify(t *testing.T) { + for _, data := range ed25519TestData { + var err error + + key, _ := ioutil.ReadFile(data.keys["public"]) + + ed25519Key, err := jwt.ParseEdPublicKeyFromPEM(key) + if err != nil { + t.Errorf("Unable to parse Ed25519 public key: %v", err) + } + + parts := strings.Split(data.tokenString, ".") + + method := jwt.GetSigningMethod(data.alg) + + err = method.Verify(strings.Join(parts[0:2], "."), parts[2], ed25519Key) + if data.valid && err != nil { + t.Errorf("[%v] Error while verifying key: %v", data.name, err) + } + if !data.valid && err == nil { + t.Errorf("[%v] Invalid key passed validation", data.name) + } + } +} + +func TestEd25519Sign(t *testing.T) { + for _, data := range ed25519TestData { + var err error + key, _ := ioutil.ReadFile(data.keys["private"]) + + ed25519Key, err := jwt.ParseEdPrivateKeyFromPEM(key) + if err != nil { + t.Errorf("Unable to parse Ed25519 private key: %v", err) + } + + parts := strings.Split(data.tokenString, ".") + + method := jwt.GetSigningMethod(data.alg) + + sig, err := method.Sign(strings.Join(parts[0:2], "."), ed25519Key) + if err != nil { + t.Errorf("[%v] Error signing token: %v", data.name, err) + } + if sig == parts[2] && !data.valid { + t.Errorf("[%v] Identical signatures\nbefore:\n%v\nafter:\n%v", data.name, parts[2], sig) + } + } +} diff --git a/ed25519_utils.go b/ed25519_utils.go new file mode 100644 index 0000000..c635727 --- /dev/null +++ b/ed25519_utils.go @@ -0,0 +1,64 @@ +package jwt + +import ( + "crypto" + "crypto/ed25519" + "crypto/x509" + "encoding/pem" + "errors" +) + +var ( + ErrNotEdPrivateKey = errors.New("Key is not a valid Ed25519 private key") + ErrNotEdPublicKey = errors.New("Key is not a valid Ed25519 public key") +) + +// Parse PEM-encoded Edwards curve private key +func ParseEdPrivateKeyFromPEM(key []byte) (crypto.PrivateKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes); err != nil { + return nil, err + } + + var pkey ed25519.PrivateKey + var ok bool + if pkey, ok = parsedKey.(ed25519.PrivateKey); !ok { + return nil, ErrNotEdPrivateKey + } + + return pkey, nil +} + +// Parse PEM-encoded Edwards curve public key +func ParseEdPublicKeyFromPEM(key []byte) (crypto.PublicKey, error) { + var err error + + // Parse PEM block + var block *pem.Block + if block, _ = pem.Decode(key); block == nil { + return nil, ErrKeyMustBePEMEncoded + } + + // Parse the key + var parsedKey interface{} + if parsedKey, err = x509.ParsePKIXPublicKey(block.Bytes); err != nil { + return nil, err + } + + var pkey ed25519.PublicKey + var ok bool + if pkey, ok = parsedKey.(ed25519.PublicKey); !ok { + return nil, ErrNotEdPublicKey + } + + return pkey, nil +} diff --git a/test/ed25519-private.pem b/test/ed25519-private.pem new file mode 100644 index 0000000..e77a58c --- /dev/null +++ b/test/ed25519-private.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIEFMEZrmlYxczXKFxIlNvNGR5JQvDhTkLovJYxwQd3ua +-----END PRIVATE KEY----- diff --git a/test/ed25519-public.pem b/test/ed25519-public.pem new file mode 100644 index 0000000..37c34c5 --- /dev/null +++ b/test/ed25519-public.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAWH7z6hpYqvPns2i4n9yymwvB3APhi4LyQ7iHOT6crtE= +-----END PUBLIC KEY-----