Signing JWTs with Go's crypto/ed25519
The crypto/ed25519 package was added to the standard library in Go 1.13. This package implements the Ed25519 Edwards-curve Digital Signature Algorithm. It offers significant speed and security improvements over RSA and it makes for a perfect signing method for JWTs. Unfortunately, the most popular JWT library for Go does not natively support it yet. It only supports ECDSA, HMAC, RSA, and RSAPSS, however, it is trivial to extend the package and satisfy the interface (github.com/dgrijalva/jwt-go).SigningMethod for EdDSA.
Implementing (github.com/dgrijalva/jwt-go).SigningMethod
type SigningMethodEd25519 struct{}
func (m *SigningMethodEd25519) Alg() string {
return "EdDSA"
}
func (m *SigningMethodEd25519) Verify(signingString string, signature string, key interface{}) error {
var err error
var sig []byte
if sig, err = jwt.DecodeSegment(signature); err != nil {
return err
}
var ed25519Key ed25519.PublicKey
var ok bool
if ed25519Key, ok = key.(ed25519.PublicKey); !ok {
return jwt.ErrInvalidKeyType
}
if len(ed25519Key) != ed25519.PublicKeySize {
return jwt.ErrInvalidKey
}
if ok := ed25519.Verify(ed25519Key, []byte(signingString), sig); !ok {
return ErrEd25519Verification
}
return nil
}
func (m *SigningMethodEd25519) Sign(signingString string, key interface{}) (str string, err error) {
var ed25519Key ed25519.PrivateKey
var ok bool
if ed25519Key, ok = key.(ed25519.PrivateKey); !ok {
return "", jwt.ErrInvalidKeyType
}
if len(ed25519Key) != ed25519.PrivateKeySize {
return "", jwt.ErrInvalidKey
}
// Sign the string and return the encoded result
sig := ed25519.Sign(ed25519Key, []byte(signingString))
return jwt.EncodeSegment(sig), nil
}
In order to work with the JWT methods above we need to have a (crypto/ed25119).PrivateKey and (crypto/ed25119).PublicKey loaded into our application to sign and verify tokens. For this case we will assume these keys are in PEM format and we can load them in from a file or environment varible.
Private and Public Keys in PEM Format
These are strings converted to []byte which is needed by (encoding/pem).Decode().
privateKeyPEM := []byte(`-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIEFMEZrmlYxczXKFxIlNvNGR5JQvDhTkLovJYxwQd3ua
-----END PRIVATE KEY-----`)
publicKeyPEM := []byte(`-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAWH7z6hpYqvPns2i4n9yymwvB3APhi4LyQ7iHOT6crtE=
-----END PUBLIC KEY-----`)
We have to parse these from their string format into actual (crypto/ed25119).PrivateKey and (crypto/ed25119).PublicKey types. We only have to do this once when our application starts up and we can pass these keys around to the functions that need them to do signing and verifying.
Decoding the Private Key
type ed25519PrivKey struct {
Version int
ObjectIdentifier struct {
ObjectIdentifier asn1.ObjectIdentifier
}
PrivateKey []byte
}
var block *pem.Block
block, _ = pem.Decode(privateKeyPEM)
var asn1PrivKey ed25519PrivKey
asn1.Unmarshal(block.Bytes, &asn1PrivKey)
privateKey := ed25519.NewKeyFromSeed(asn1PrivKey.PrivateKey[2:])
Decoding the Public Key
type ed25519PubKey struct {
OBjectIdentifier struct {
ObjectIdentifier asn1.ObjectIdentifier
}
PublicKey asn1.BitString
}
var block *pem.Block
block, _ = pem.Decode(publicKeyPEM)
var asn1PubKey ed25519PubKey
asn1.Unmarshal(block.Bytes, &asn1PubKey)
publicKey := ed25519.PublicKey(asn1PubKey.PublicKey.Bytes)
Now that we have implemented the (github.com/dgrijalva/jwt-go).SigningMethod and have a (crypto/ed25519).PrivateKey and (crypto/ed25519).PublicKey we can put this all together to sign and verify JWTs.
var Ed25519SigningMethod SigningMethodEd25519
jwt.RegisterSigningMethod(Ed25519SigningMethod.Alg(), func() jwt.SigningMethod { return &Ed25519SigningMethod })
token := jwt.NewWithClaims(&Ed25519SigningMethod, jwt.StandardClaims{
IssuedAt: time.Now().Unix(),
Issuer: "urn:ed25519-jwt",
Subject: "someone@example.com",
})
jwtstring := token.SignedString(privateKey)
fmt.Println(jwtstring)
// Outputs: eyJhbGciOiJFRDI1NTE5IiwidHlwI...
token, _ := jwt.Parse(jwtstring, func(token *jwt.Token) (interface{}, error) {
return publicKey, nil
})
fmt.Println(token.Issuer, token.Subject)
// Outputs: urn:ed25519-jwt someone@example.com
The jwtstring
can be used by clients to verify their identity as any other JWT, but now we are using Ed25519 for signing and verifying tokens like you can with RSA. The https://jwt.io website is a great tool for working with JWTs and you can see other libraries that support different signing methods. Since Ed25519 is fairly new you will notice that a lot are missing the "EdDSA" algorithm, but now you can at least implement this yourself with not a lot of code or importing a 3rd party dependency.