From 8bd975cc42c2fef6c7d004ee3492d981ba467c06 Mon Sep 17 00:00:00 2001 From: Ryan Sundberg Date: Mon, 14 Oct 2024 15:23:18 -0700 Subject: [PATCH] Add support for `EdDSA` JWT signatures --- README.md | 1 + go.mod | 2 +- plugin/backend.go | 2 ++ plugin/config.go | 4 +++- plugin/path_jwks.go | 18 ++++++++++++++++-- plugin/policy_signer.go | 17 ++++++++++++----- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b2169cd..91eff77 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ verification as long as any JWTs signed with them are valid. The plugin supports a subset of the asymmetric encryption algorithms outlined in the JWT specification. +* EdDSA * ES256 * ES384 * ES512 diff --git a/go.mod b/go.mod index 73dc5ce..70febdd 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/hashicorp/vault/api v1.10.0 github.com/hashicorp/vault/sdk v0.10.2 github.com/mariuszs/friendlyid-go v0.0.0-20200911181514-555cced97798 + golang.org/x/crypto v0.12.0 gopkg.in/square/go-jose.v2 v2.6.0 ) @@ -65,7 +66,6 @@ require ( github.com/ryanuber/go-glob v1.0.0 // indirect github.com/stretchr/testify v1.8.3 // indirect go.uber.org/atomic v1.9.0 // indirect - golang.org/x/crypto v0.12.0 // indirect golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect diff --git a/plugin/backend.go b/plugin/backend.go index 78d5b75..633c041 100644 --- a/plugin/backend.go +++ b/plugin/backend.go @@ -169,6 +169,8 @@ func (b *backend) getPolicy(ctx context.Context, stg logical.Storage, config *Co default: err = errutil.InternalError{Err: "unsupported RSA key size"} } + case jose.EdDSA: + polReq.KeyType = keysutil.KeyType_ED25519 case jose.ES256: polReq.KeyType = keysutil.KeyType_ECDSA_P256 case jose.ES384: diff --git a/plugin/config.go b/plugin/config.go index 9a3b89a..a34cd62 100644 --- a/plugin/config.go +++ b/plugin/config.go @@ -48,7 +48,7 @@ var DefaultAllowedClaims = []string{"sub", "aud"} var ReservedClaims = []string{"iss", "exp", "nbf", "iat", "jti"} var ReservedHeaders = []string{"kid", "alg", "enc", "zip", "crit"} -var AllowedSignatureAlgorithmNames = []string{string(jose.ES256), string(jose.ES384), string(jose.ES512), string(jose.RS256), string(jose.RS384), string(jose.RS512)} +var AllowedSignatureAlgorithmNames = []string{string(jose.EdDSA), string(jose.ES256), string(jose.ES384), string(jose.ES512), string(jose.RS256), string(jose.RS384), string(jose.RS512)} var AllowedRSAKeyBits = []int{2048, 3072, 4096} // Config holds all configuration for the backend. @@ -186,6 +186,8 @@ func (b *backend) saveConfig(ctx context.Context, stg logical.Storage, config *C default: err = errutil.InternalError{Err: "unsupported RSA key size"} } + case jose.EdDSA: + policy.Type = keysutil.KeyType_ED25519 case jose.ES256: policy.Type = keysutil.KeyType_ECDSA_P256 case jose.ES384: diff --git a/plugin/path_jwks.go b/plugin/path_jwks.go index 879b21c..41505df 100644 --- a/plugin/path_jwks.go +++ b/plugin/path_jwks.go @@ -19,10 +19,13 @@ package jwtsecrets import ( "context" "crypto/x509" + "encoding/base64" "encoding/json" "encoding/pem" "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/helper/keysutil" "github.com/hashicorp/vault/sdk/logical" + "golang.org/x/crypto/ed25519" "gopkg.in/square/go-jose.v2" "strconv" ) @@ -84,6 +87,7 @@ func (b *backend) getPublicKeys(ctx context.Context, stg logical.Storage, mount Keys: make([]jose.JSONWebKey, keyCount), } + logger := b.Logger() keyIdx := 0 for version := policy.MinDecryptionVersion; version <= policy.LatestVersion; version++ { @@ -92,16 +96,26 @@ func (b *backend) getPublicKeys(ctx context.Context, stg logical.Storage, mount continue } - if key.FormattedPublicKey != "" { + if policy.Type == keysutil.KeyType_ED25519 { + keyBytes, err := base64.StdEncoding.DecodeString(key.FormattedPublicKey) + if err != nil { + logger.Error("Failed to decode ED25519 public key", "public_key", key.FormattedPublicKey, "error", err) + continue + } + jwkSet.Keys[keyIdx].Key = ed25519.PublicKey(keyBytes) + } else if key.FormattedPublicKey != "" { block, _ := pem.Decode([]byte(key.FormattedPublicKey)) if block == nil { + logger.Error("Failed to decode PEM key", "public_key", key.FormattedPublicKey) continue } - jwkSet.Keys[keyIdx].Key, err = x509.ParsePKIXPublicKey(block.Bytes) + publicKey, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { + logger.Error("Failed to parse PKIX public key", "public_key", key.FormattedPublicKey, "error", err) continue } + jwkSet.Keys[keyIdx].Key = publicKey } else if key.RSAKey != nil { jwkSet.Keys[keyIdx].Key = &key.RSAKey.PublicKey } diff --git a/plugin/policy_signer.go b/plugin/policy_signer.go index 177660d..87af867 100644 --- a/plugin/policy_signer.go +++ b/plugin/policy_signer.go @@ -102,6 +102,9 @@ func (ps *PolicySigner) sign(input []byte) ([]byte, error) { hashType = keysutil.HashTypeSHA2512 hash = crypto.SHA512 sigAlg = "pkcs1v15" + case jose.EdDSA: + hashType = keysutil.HashTypeNone + sigAlg = "" case jose.ES256: hashType = keysutil.HashTypeSHA2256 hash = crypto.SHA256 @@ -120,11 +123,15 @@ func (ps *PolicySigner) sign(input []byte) ([]byte, error) { keyVersion := ps.Policy.LatestVersion - hasher := hash.New() - - // According to documentation, Write() on hash never fails - _, _ = hasher.Write(input) - hashedInput := hasher.Sum(nil) + var hashedInput []byte + if hashType == keysutil.HashTypeNone { + hashedInput = input + } else { + hasher := hash.New() + // According to documentation, Write() on hash never fails + _, _ = hasher.Write(input) + hashedInput = hasher.Sum(nil) + } result, err := ps.Policy.Sign(keyVersion, nil, hashedInput, hashType, sigAlg, keysutil.MarshalingTypeJWS) if err != nil {