mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat: allow ECDSA and ED25519 public keys (#11819)
# Which Problems Are Solved Currently ZITADEL only allows the use of RSA public keys for machine user authentication (jwt-bearer grant), including for system API users. Attempting to use ECDSA (e.g. P-256) or ED25519 keys results in `Errors.Internal` because `BytesToPublicKey` performs an `ifc.(*rsa.PublicKey)` type assertion that returns `(nil, nil)` for non-RSA keys, which then causes a nil key panic in go-jose during JWT verification. This is the same fix as #8433 (by @livio-a), rebased onto current `main`. # How the Problems Are Solved - `BytesToPublicKey` now returns `crypto.PublicKey` (the standard library interface) instead of `*rsa.PublicKey` - A type switch validates the parsed key is one of `*rsa.PublicKey`, `*ecdsa.PublicKey`, or `ed25519.PublicKey` - A new `ErrNoPublicKey` sentinel error is returned for unsupported key types instead of silently returning nil - Callers in `system_token.go` and `query/key.go` are updated to use the generic `crypto.PublicKey` interface # Additional Changes None # Additional Context Duplicate of #8433 which has been open since August 2024. We hit this bug while implementing OIDC bootstrap for an SGX enclave that generates ECDSA P-256 keys at runtime -- the `AddKey` API accepts the ECDSA SPKI PEM fine, but the subsequent `jwt-bearer` token exchange fails with `Errors.Internal` due to the nil key. --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Wim Van Laer <[email protected]>
This commit is contained in:
co-authored by
Copilot
Wim Van Laer
parent
9135ff4b36
commit
b558a1f79f
@@ -2,6 +2,7 @@ package authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
@@ -13,7 +14,7 @@ import (
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
zcrypto "github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
@@ -89,7 +90,7 @@ type SystemAPIUser struct {
|
||||
}
|
||||
|
||||
type SystemAPIPublicKey struct {
|
||||
Data *rsa.PublicKey
|
||||
Data crypto.PublicKey
|
||||
NotBefore *time.Time
|
||||
NotAfter *time.Time
|
||||
}
|
||||
@@ -104,7 +105,7 @@ func (s *SystemAPIUser) readKey() (*SystemAPIPublicKey, error) {
|
||||
}
|
||||
|
||||
// when an RSA key is provided, use the raw data
|
||||
key, err := crypto.BytesToPublicKey(s.KeyData)
|
||||
key, err := zcrypto.BytesToPublicKey(s.KeyData)
|
||||
if err == nil {
|
||||
return &SystemAPIPublicKey{Data: key}, nil
|
||||
}
|
||||
|
||||
+12
-6
@@ -3,6 +3,8 @@ package crypto
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
@@ -205,8 +207,9 @@ func BytesToPrivateKeyPKCS8[T crypto.PrivateKey](priv []byte) (T, error) {
|
||||
}
|
||||
|
||||
var ErrEmpty = errors.New("cannot decode, empty data")
|
||||
var ErrNoPublicKey = errors.New("unsupported public key type")
|
||||
|
||||
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
|
||||
func BytesToPublicKey(pub []byte) (crypto.PublicKey, error) {
|
||||
if len(pub) == 0 {
|
||||
return nil, ErrEmpty
|
||||
}
|
||||
@@ -214,15 +217,18 @@ func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
|
||||
if block == nil {
|
||||
return nil, ErrEmpty
|
||||
}
|
||||
ifc, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, ok := ifc.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, err
|
||||
switch key.(type) {
|
||||
case *rsa.PublicKey,
|
||||
*ecdsa.PublicKey,
|
||||
ed25519.PublicKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, ErrNoPublicKey
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func EncryptKeys(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, alg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, error) {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mustGenerateRSAPEM(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})
|
||||
}
|
||||
|
||||
func mustGenerateECDSAPEM(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
der, err := x509.MarshalPKIXPublicKey(&key.PublicKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})
|
||||
}
|
||||
|
||||
func mustGenerateEd25519PEM(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
pub, _, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
der, err := x509.MarshalPKIXPublicKey(pub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_RSA(t *testing.T) {
|
||||
pemData := mustGenerateRSAPEM(t)
|
||||
key, err := BytesToPublicKey(pemData)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if _, ok := key.(*rsa.PublicKey); !ok {
|
||||
t.Fatalf("expected *rsa.PublicKey, got %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_ECDSA(t *testing.T) {
|
||||
pemData := mustGenerateECDSAPEM(t)
|
||||
key, err := BytesToPublicKey(pemData)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if _, ok := key.(*ecdsa.PublicKey); !ok {
|
||||
t.Fatalf("expected *ecdsa.PublicKey, got %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_Ed25519(t *testing.T) {
|
||||
pemData := mustGenerateEd25519PEM(t)
|
||||
key, err := BytesToPublicKey(pemData)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if _, ok := key.(ed25519.PublicKey); !ok {
|
||||
t.Fatalf("expected ed25519.PublicKey, got %T", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_Empty(t *testing.T) {
|
||||
_, err := BytesToPublicKey(nil)
|
||||
if !errors.Is(err, ErrEmpty) {
|
||||
t.Fatalf("expected ErrEmpty, got %v", err)
|
||||
}
|
||||
_, err = BytesToPublicKey([]byte{})
|
||||
if !errors.Is(err, ErrEmpty) {
|
||||
t.Fatalf("expected ErrEmpty, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_InvalidPEM(t *testing.T) {
|
||||
_, err := BytesToPublicKey([]byte("not a PEM block"))
|
||||
if !errors.Is(err, ErrEmpty) {
|
||||
t.Fatalf("expected ErrEmpty for non-PEM data, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytesToPublicKey_MalformedDER(t *testing.T) {
|
||||
malformed := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: []byte("invalid DER data")})
|
||||
_, err := BytesToPublicKey(malformed)
|
||||
if err == nil {
|
||||
t.Fatal("expected parse error for malformed DER, got nil")
|
||||
}
|
||||
}
|
||||
@@ -3,21 +3,21 @@ package query
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
zcrypto "github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/query/projection"
|
||||
)
|
||||
|
||||
type Key interface {
|
||||
ID() string
|
||||
Algorithm() string
|
||||
Use() crypto.KeyUsage
|
||||
Use() zcrypto.KeyUsage
|
||||
Sequence() uint64
|
||||
}
|
||||
|
||||
type PrivateKey interface {
|
||||
Key
|
||||
Expiry() time.Time
|
||||
Key() *crypto.CryptoValue
|
||||
Key() *zcrypto.CryptoValue
|
||||
}
|
||||
|
||||
type PublicKey interface {
|
||||
@@ -38,7 +38,7 @@ type key struct {
|
||||
sequence uint64
|
||||
resourceOwner string
|
||||
algorithm string
|
||||
use crypto.KeyUsage
|
||||
use zcrypto.KeyUsage
|
||||
}
|
||||
|
||||
func (k *key) ID() string {
|
||||
@@ -49,7 +49,7 @@ func (k *key) Algorithm() string {
|
||||
return k.algorithm
|
||||
}
|
||||
|
||||
func (k *key) Use() crypto.KeyUsage {
|
||||
func (k *key) Use() zcrypto.KeyUsage {
|
||||
return k.use
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user