Files
b558a1f79f 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]>
2026-04-09 11:30:23 +02:00

112 lines
2.6 KiB
Go

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")
}
}