feat: support PKCS#8

This commit is contained in:
Livio Spring 2024-07-05 12:47:05 +02:00
parent fc6716bf22
commit c85ef9f9df
No known key found for this signature in database
GPG key ID: 26BB1C2FA5952CF0
4 changed files with 134 additions and 37 deletions

View file

@ -12,11 +12,12 @@ import (
"github.com/go-jose/go-jose/v4" "github.com/go-jose/go-jose/v4"
"github.com/zitadel/logging" "github.com/zitadel/logging"
"go.opentelemetry.io/otel"
"golang.org/x/oauth2"
"github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/crypto"
httphelper "github.com/zitadel/oidc/v3/pkg/http" httphelper "github.com/zitadel/oidc/v3/pkg/http"
"github.com/zitadel/oidc/v3/pkg/oidc" "github.com/zitadel/oidc/v3/pkg/oidc"
"go.opentelemetry.io/otel"
"golang.org/x/oauth2"
) )
var ( var (
@ -196,12 +197,12 @@ func CallTokenExchangeEndpoint(ctx context.Context, request any, authFn any, cal
} }
func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) { func NewSignerFromPrivateKeyByte(key []byte, keyID string) (jose.Signer, error) {
privateKey, err := crypto.BytesToPrivateKey(key) privateKey, algorithm, err := crypto.BytesToPrivateKey(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signingKey := jose.SigningKey{ signingKey := jose.SigningKey{
Algorithm: jose.RS256, Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: keyID},
} }
return jose.NewSigner(signingKey, &jose.SignerOptions{}) return jose.NewSigner(signingKey, &jose.SignerOptions{})

View file

@ -1,22 +1,45 @@
package crypto package crypto
import ( import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"encoding/pem" "encoding/pem"
"errors" "errors"
"github.com/go-jose/go-jose/v4"
) )
func BytesToPrivateKey(b []byte) (*rsa.PrivateKey, error) { var (
ErrPEMDecode = errors.New("PEM decode failed")
ErrUnsupportedFormat = errors.New("key is neither in PKCS#1 nor PKCS#8 format")
ErrUnsupportedPrivateKey = errors.New("unsupported key type, must be RSA, ECDSA or ED25519 private key")
)
func BytesToPrivateKey(b []byte) (crypto.PublicKey, jose.SignatureAlgorithm, error) {
block, _ := pem.Decode(b) block, _ := pem.Decode(b)
if block == nil { if block == nil {
return nil, errors.New("PEM decode failed") return nil, "", ErrPEMDecode
} }
key, err := x509.ParsePKCS1PrivateKey(block.Bytes) privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err == nil {
return privateKey, jose.RS256, nil
}
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil { if err != nil {
return nil, err return nil, "", ErrUnsupportedFormat
}
switch privateKey := key.(type) {
case *rsa.PrivateKey:
return privateKey, jose.RS256, nil
case ed25519.PrivateKey:
return privateKey, jose.EdDSA, nil
case *ecdsa.PrivateKey:
return privateKey, jose.ES256, nil
default:
return nil, "", ErrUnsupportedPrivateKey
} }
return key, nil
} }

View file

@ -1,21 +1,64 @@
package crypto_test package crypto_test
import ( import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"testing" "testing"
"github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/zitadel/oidc/v3/pkg/crypto" zcrypto "github.com/zitadel/oidc/v3/pkg/crypto"
) )
func TestBytesToPrivateKey(tt *testing.T) { func TestBytesToPrivateKey(t *testing.T) {
tt.Run("PEMDecodeError", func(t *testing.T) { type args struct {
_, err := crypto.BytesToPrivateKey([]byte("The non-PEM sequence")) key []byte
assert.EqualError(t, err, "PEM decode failed") }
}) type want struct {
key crypto.Signer
tt.Run("InvalidKeyFormat", func(t *testing.T) { algorithm jose.SignatureAlgorithm
_, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN PRIVATE KEY----- err error
}
tests := []struct {
name string
args args
want want
}{
{
name: "PEMDecodeError",
args: args{
key: []byte("The non-PEM sequence"),
},
want: want{
err: zcrypto.ErrPEMDecode,
},
},
{
name: "PKCS#1 RSA",
args: args{
key: []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END RSA PRIVATE KEY-----`),
},
want: want{
key: &rsa.PrivateKey{},
algorithm: jose.RS256,
err: nil,
},
},
{
name: "PKCS#8 RSA",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCfaDB7pK/fmP/I
7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL 7IusSK8lTCBnPZghqIbVLt2QHYAMoEF1CaF4F4rxo2vl1Mt8gwsq4T3osQFZMvnL
YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx YHb7KNyUoJgTjLxJQADv2u4Q3U38heAzK5Tp4ry4MCnuyJIqAPK1GiruwEq4zQrx
@ -42,21 +85,50 @@ srJnjF0H8oKmAY6hw+1Tm/n/b08p+RyL48TgVSE2vhUCgYA3BWpkD4PlCcn/FZsq
OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR OrLFyFXI6jIaxskFtsRW1IxxIlAdZmxfB26P/2gx6VjLdxJI/RRPkJyEN2dP7CbR
BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9 BDjb565dy1O9D6+UrY70Iuwjz+OcALRBBGTaiF2pLn6IhSzNI2sy/tXX8q8dBlg9
OFCrqT/emes3KytTPfa5NZtYeQ== OFCrqT/emes3KytTPfa5NZtYeQ==
-----END PRIVATE KEY-----`)) -----END PRIVATE KEY-----`),
assert.EqualError(t, err, "x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)") },
}) want: want{
key: &rsa.PrivateKey{},
algorithm: jose.RS256,
err: nil,
},
},
{
name: "PKCS#8 ECDSA",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwwOZSU4GlP7ps/Wp
V6o0qRwxultdfYo/uUuj48QZjSuhRANCAATMiI2Han+ABKmrk5CNlxRAGC61w4d3
G4TAeuBpyzqJ7x/6NjCxoQzJzZHtNjIfjVATI59XFZWF59GhtSZbShAr
-----END PRIVATE KEY-----`),
},
want: want{
key: &ecdsa.PrivateKey{},
algorithm: jose.ES256,
err: nil,
},
},
{
name: "PKCS#8 ED25519",
args: args{
key: []byte(`-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIHu6ZtDsjjauMasBxnS9Fg87UJwKfcT/oiq6S0ktbky8
-----END PRIVATE KEY-----`),
},
want: want{
key: ed25519.PrivateKey{},
algorithm: jose.EdDSA,
err: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
key, algorithm, err := zcrypto.BytesToPrivateKey(tt.args.key)
assert.IsType(t, tt.want.key, key)
assert.Equal(t, tt.want.algorithm, algorithm)
assert.ErrorIs(t, tt.want.err, err)
})
tt.Run("Ok", func(t *testing.T) { }
key, err := crypto.BytesToPrivateKey([]byte(`-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
o3qGy0t6z09AIJtH+5OeRV1be+N4cDYJKffGzDa88vQENZiRm0GRq6a+HPGQMd2k
TQIhAKMSvzIBnni7ot/OSie2TmJLY4SwTQAevXysE2RbFDYdAiEBCUEaRQnMnbp7
9mxDXDf6AU0cN/RPBjb9qSHDcWZHGzUCIG2Es59z8ugGrDY+pxLQnwfotadxd+Uy
v/Ow5T0q5gIJAiEAyS4RaI9YG8EWx/2w0T67ZUVAw8eOMB6BIUg0Xcu+3okCIBOs
/5OiPgoTdSy7bcF9IGpSE8ZgGKzgYQVZeN97YE00
-----END RSA PRIVATE KEY-----`))
assert.NoError(t, err)
assert.NotNil(t, key)
})
} }

View file

@ -9,6 +9,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/muhlemmer/gu" "github.com/muhlemmer/gu"
"github.com/zitadel/oidc/v3/pkg/crypto" "github.com/zitadel/oidc/v3/pkg/crypto"
) )
@ -344,12 +345,12 @@ func AppendClientIDToAudience(clientID string, audience []string) []string {
} }
func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) { func GenerateJWTProfileToken(assertion *JWTProfileAssertionClaims) (string, error) {
privateKey, err := crypto.BytesToPrivateKey(assertion.PrivateKey) privateKey, algorithm, err := crypto.BytesToPrivateKey(assertion.PrivateKey)
if err != nil { if err != nil {
return "", err return "", err
} }
key := jose.SigningKey{ key := jose.SigningKey{
Algorithm: jose.RS256, Algorithm: algorithm,
Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID}, Key: &jose.JSONWebKey{Key: privateKey, KeyID: assertion.PrivateKeyID},
} }
signer, err := jose.NewSigner(key, &jose.SignerOptions{}) signer, err := jose.NewSigner(key, &jose.SignerOptions{})