diff --git a/internal/gatewayapi/tls.go b/internal/gatewayapi/tls.go index cd525acc4f..ab0878baba 100644 --- a/internal/gatewayapi/tls.go +++ b/internal/gatewayapi/tls.go @@ -105,7 +105,7 @@ func parseCertsFromTLSSecretsData(secrets []*corev1.Secret) ([]*corev1.Secret, [ keyData := secret.Data[corev1.TLSPrivateKeyKey] - keyBlock, _ := pem.Decode(keyData) + keyBlock := firstSupportedPrivateKeyBlock(keyData) if keyBlock == nil { errs = append(errs, fmt.Errorf("%s/%s must contain valid %s and %s, unable to decode pem data in %s", secret.Namespace, secret.Name, corev1.TLSCertKey, corev1.TLSPrivateKeyKey, corev1.TLSPrivateKeyKey)) @@ -211,6 +211,29 @@ func parseCertsFromTLSSecretsData(secrets []*corev1.Secret) ([]*corev1.Secret, [ return validSecrets, certs, nil } +// firstSupportedPrivateKeyBlock returns the first supported private key PEM block, +// skipping preceding non-key blocks such as OpenSSL's "EC PARAMETERS" block. +// It returns the first block when no supported key is found so the caller can +// report the unexpected block type. +func firstSupportedPrivateKeyBlock(data []byte) *pem.Block { + var firstBlock *pem.Block + for len(data) > 0 { + block, rest := pem.Decode(data) + if block == nil { + return firstBlock + } + if firstBlock == nil { + firstBlock = block + } + switch block.Type { + case "PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY": + return block + } + data = rest + } + return firstBlock +} + // validateCertBlock parses the certificate(s) in a single PEM block and returns // an error if the block is not a valid certificate or if any certificate in it // is expired or not yet valid (outside its NotBefore/NotAfter window). diff --git a/internal/gatewayapi/tls_test.go b/internal/gatewayapi/tls_test.go index b76973d3fc..48b91c1c0b 100644 --- a/internal/gatewayapi/tls_test.go +++ b/internal/gatewayapi/tls_test.go @@ -332,6 +332,37 @@ func TestValidateTLSSecretsData(t *testing.T) { } } +func TestValidateTLSSecretsDataWithECParameters(t *testing.T) { + const ecParametersPEM = "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n" + + t.Run("ec-parameters-before-private-key", func(t *testing.T) { + secrets := createTestSecrets(t, []string{"ecdsa-p256-cert.pem"}, []string{"ecdsa-p256.key"}) + keyData := append([]byte(nil), secrets[0].Data[corev1.TLSPrivateKeyKey]...) + secrets[0].Data[corev1.TLSPrivateKeyKey] = append([]byte(ecParametersPEM), keyData...) + + validSecrets, certs, err := parseCertsFromTLSSecretsData(secrets) + require.NoError(t, err) + require.Len(t, validSecrets, 1) + require.Len(t, certs, 1) + + keyBlock, _ := pem.Decode(keyData) + require.NotNil(t, keyBlock) + require.Equal(t, pem.EncodeToMemory(keyBlock), validSecrets[0].Data[corev1.TLSPrivateKeyKey]) + }) + + t.Run("ec-parameters-without-private-key", func(t *testing.T) { + secrets := createTestSecrets(t, []string{"ecdsa-p256-cert.pem"}, []string{"ecdsa-p256.key"}) + secrets[0].Data[corev1.TLSPrivateKeyKey] = []byte(ecParametersPEM) + + validSecrets, certs, err := parseCertsFromTLSSecretsData(secrets) + require.Error(t, err) + require.Equal(t, "test/secret must contain valid tls.crt and tls.key, EC PARAMETERS key format found in tls.key, supported formats are PKCS1, PKCS8 or EC", err.Error()) + require.Equal(t, gwapiv1.ListenerReasonInvalidCertificateRef, err.Reason()) + require.Empty(t, validSecrets) + require.Empty(t, certs) + }) +} + func TestFilterValidCertificates(t *testing.T) { type testCase struct { Name string diff --git a/release-notes/current/bug_fixes/9338-tls-key-ec-parameters.md b/release-notes/current/bug_fixes/9338-tls-key-ec-parameters.md new file mode 100644 index 0000000000..4979cd4bac --- /dev/null +++ b/release-notes/current/bug_fixes/9338-tls-key-ec-parameters.md @@ -0,0 +1 @@ +Fixed Gateway TLS Secret validation rejecting valid ECDSA private keys when the `tls.key` includes an `EC PARAMETERS` PEM block before the private key.