feat(auth): support per-deploy login password key override
Multi-replica deployments need every Pod/container to share one RSA private key, otherwise public keys handed to the browser may not match the key that decrypts the resulting ciphertext. Introduce a `*.local.yaml` override layer that ops-config and tenant-config both load on top of the base config, ship example overrides plus compose/k3s wiring, and tolerate PEM bodies that arrive folded or escaped from Secrets.
This commit is contained in:
+4
-1
@@ -6,6 +6,8 @@ dist/
|
||||
.env
|
||||
.env.local
|
||||
.env.signing
|
||||
deploy/config.local.yaml
|
||||
deploy/ops-config.local.yaml
|
||||
apps/*/.env.signing
|
||||
apps/*/dist/
|
||||
apps/*/out/
|
||||
@@ -23,4 +25,5 @@ apps/*/firefox-mv*/
|
||||
*.tmp
|
||||
apps/*/stats.html
|
||||
ops-api
|
||||
tmp/*
|
||||
tmp/*
|
||||
*.pem
|
||||
|
||||
@@ -40,3 +40,15 @@ Default ports are NAS-friendly:
|
||||
- MinIO console: `19001`
|
||||
|
||||
Edit `.env` before rerunning `bash deploy.sh` if you need real API keys, stronger secrets, or different ports.
|
||||
|
||||
For multi-container / multi-Pod login password encryption, put the same RSA private key in every replica's config override:
|
||||
|
||||
- tenant API: copy `config.local.yaml.example` to `config.local.yaml`, then fill `auth.password_cipher.private_key_pem`
|
||||
- ops API: copy `ops-config.local.yaml.example` to `ops-config.local.yaml`, then fill `auth.password_cipher.private_key_pem`
|
||||
|
||||
Generate keys with:
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out login-password-rsa.pem
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ops-login-password-rsa.pem
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
auth:
|
||||
password_cipher:
|
||||
enabled: true
|
||||
key_id: login-password-rsa-oaep-v1
|
||||
# 多 Pod / 多容器部署必须使用同一把私钥。复制本文件为 config.local.yaml,
|
||||
# 再粘贴完整 PEM;不要提交生产私钥。留空时应用会生成单进程临时私钥,
|
||||
# 只适合本机单副本试跑,不适合多副本生产。
|
||||
#
|
||||
# 生成示例:
|
||||
# openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out login-password-rsa.pem
|
||||
private_key_pem: ""
|
||||
@@ -169,6 +169,13 @@ jwt:
|
||||
access_ttl: 15m
|
||||
refresh_ttl: 720h
|
||||
|
||||
auth:
|
||||
password_cipher:
|
||||
# 生产多实例请在 config.local.yaml 中配置同一把 private_key_pem。
|
||||
enabled: true
|
||||
key_id: login-password-rsa-oaep-v1
|
||||
private_key_pem: ""
|
||||
|
||||
log:
|
||||
level: info,warn,error
|
||||
format: json
|
||||
|
||||
@@ -22,6 +22,7 @@ x-app-env: &app-env
|
||||
|
||||
x-app-volumes: &app-volumes
|
||||
- ./config.yaml:/app/configs/config.yaml:ro
|
||||
- ./config.local.yaml:/app/configs/config.local.yaml:ro
|
||||
- ./prompts.yml:/app/configs/prompts.yml:ro
|
||||
|
||||
x-app-depends: &app-depends
|
||||
@@ -226,6 +227,7 @@ services:
|
||||
CONFIG_PATH: /app/configs/config.yaml
|
||||
volumes:
|
||||
- ./config.yaml:/app/configs/config.yaml:ro
|
||||
- ./config.local.yaml:/app/configs/config.local.yaml:ro
|
||||
- ./prompts.yml:/app/configs/prompts.yml:ro
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
@@ -355,6 +357,7 @@ services:
|
||||
OPS_DEFAULT_ADMIN_EMAIL: ${OPS_DEFAULT_ADMIN_EMAIL:-}
|
||||
volumes:
|
||||
- ./ops-config.yaml:/app/configs/ops-config.yaml:ro
|
||||
- ./ops-config.local.yaml:/app/configs/ops-config.local.yaml:ro
|
||||
depends_on:
|
||||
pgbouncer:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -36,11 +36,22 @@ sudo k3s ctr images import images.tar
|
||||
- `SCHEDULER_INTERNAL_METRICS_TOKEN`
|
||||
- `BROWSER_FETCH_TOKEN`
|
||||
- `POSTGRES_PASSWORD` / `RABBITMQ_DEFAULT_PASS` / `MINIO_ROOT_*`
|
||||
- `config.local.yaml` 里的 `auth.password_cipher.private_key_pem`
|
||||
- `ops-config.local.yaml` 里的 `auth.password_cipher.private_key_pem`
|
||||
- `config/config.yaml` 里的 LLM / SiliconFlow API key
|
||||
- `config.local.yaml` 里对应的数据库、RabbitMQ、MinIO、JWT 配置
|
||||
|
||||
注意:应用运行时通过 `pgbouncer` / `monitoring-pgbouncer` 访问数据库;`migrate` 任务仍然直连 `postgres` / `monitoring-postgres`。数据库密码统一来自 `POSTGRES_PASSWORD` secret,应用配置和 PgBouncer 都会跟随这个 secret。
|
||||
|
||||
登录密码加密使用 RSA-OAEP-SHA256。多 Pod 部署时,同一类服务的所有副本必须读取同一把私钥和同一个 `key_id`,否则某个 Pod 下发的公钥对应密文可能被另一个 Pod 解不开。建议租户端和 ops 端分别生成不同私钥:
|
||||
|
||||
```bash
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out login-password-rsa.pem
|
||||
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ops-login-password-rsa.pem
|
||||
```
|
||||
|
||||
然后把完整 PEM 分别粘贴到 `deploy/k3s/secrets.yaml` 的 `config.local.yaml` 和 `ops-config.local.yaml`。
|
||||
|
||||
## 3. 部署
|
||||
|
||||
```bash
|
||||
|
||||
@@ -556,6 +556,11 @@ spec:
|
||||
items:
|
||||
- key: ops-config.yaml
|
||||
path: ops-config.yaml
|
||||
- secret:
|
||||
name: geo-rankly-app-secret
|
||||
items:
|
||||
- key: ops-config.local.yaml
|
||||
path: ops-config.local.yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
server:
|
||||
port: 8080
|
||||
mode: release
|
||||
# 生产环境请配置为真实前端域名,例如 https://app.example.com。
|
||||
# 留空时沿用开发环境 CORS 放开策略。
|
||||
allowed_origins: []
|
||||
security_headers:
|
||||
enabled: true
|
||||
trusted_proxies:
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
@@ -30,6 +35,47 @@ monitoring_database:
|
||||
|
||||
rabbitmq:
|
||||
url: amqp://geo:geo_dev@rabbitmq:5672/geo
|
||||
monitor_result_exchange: monitor.result
|
||||
monitor_result_routing_key: monitor.result.ingest
|
||||
monitor_result_queue: monitor.result.ingest
|
||||
monitor_result_dlx: monitor.result.dlx
|
||||
monitor_result_dlq: monitor.result.ingest.dlq
|
||||
monitor_result_dlq_routing_key: monitor.result.ingest.dlq
|
||||
monitor_projection_exchange: monitor.projection
|
||||
monitor_projection_routing_key: monitor.projection.rebuild
|
||||
monitor_projection_queue: monitor.projection.rebuild
|
||||
monitor_projection_dlx: monitor.projection.dlx
|
||||
monitor_projection_dlq: monitor.projection.rebuild.dlq
|
||||
monitor_projection_dlq_routing_key: monitor.projection.rebuild.dlq
|
||||
generation_exchange: generation.task
|
||||
generation_routing_key: generation.task.run
|
||||
generation_queue: generation.task.run
|
||||
generation_dlx: generation.task.dlx
|
||||
generation_dlq: generation.task.run.dlq
|
||||
generation_dlq_routing_key: generation.task.run.dlq
|
||||
generation_event_exchange: generation.event
|
||||
desktop_task_event_exchange: desktop.task.event
|
||||
desktop_dispatch_exchange: desktop.task.dispatch
|
||||
desktop_dispatch_publish_prefix: publish
|
||||
desktop_dispatch_monitor_prefix: monitor
|
||||
desktop_account_health_exchange: desktop.account.health
|
||||
desktop_account_health_routing_key: desktop.account.health.report
|
||||
desktop_account_health_queue: desktop.account.health.report
|
||||
desktop_account_health_dlx: desktop.account.health.dlx
|
||||
desktop_account_health_dlq: desktop.account.health.report.dlq
|
||||
desktop_account_health_dlq_routing_key: desktop.account.health.report.dlq
|
||||
template_assist_exchange: template.assist
|
||||
template_assist_routing_key: template.assist.run
|
||||
template_assist_queue: template.assist.run
|
||||
template_assist_dlx: template.assist.dlx
|
||||
template_assist_dlq: template.assist.run.dlq
|
||||
template_assist_dlq_routing_key: template.assist.run.dlq
|
||||
kol_assist_exchange: kol.assist
|
||||
kol_assist_routing_key: kol.assist.run
|
||||
kol_assist_queue: kol.assist.run
|
||||
kol_assist_dlx: kol.assist.dlx
|
||||
kol_assist_dlq: kol.assist.run.dlq
|
||||
kol_assist_dlq_routing_key: kol.assist.run.dlq
|
||||
compliance_review_exchange: compliance.review
|
||||
compliance_review_routing_key: compliance.review.run
|
||||
compliance_review_queue: compliance.review.run
|
||||
@@ -53,6 +99,9 @@ monitoring_workers:
|
||||
result_ingest_concurrency: 4
|
||||
projection_rebuild_concurrency: 2
|
||||
|
||||
monitoring_dispatch:
|
||||
desktop_tasks_rollout_percentage: 5
|
||||
|
||||
membership:
|
||||
default_plan_code: free
|
||||
plans:
|
||||
@@ -96,7 +145,6 @@ brand_library:
|
||||
max_keywords: 5
|
||||
max_questions_per_keyword: 5
|
||||
|
||||
|
||||
redis:
|
||||
addr: redis:6379
|
||||
db: 0
|
||||
@@ -126,6 +174,16 @@ jwt:
|
||||
access_ttl: 15m
|
||||
refresh_ttl: 720h
|
||||
|
||||
auth:
|
||||
login_guard:
|
||||
enabled: true
|
||||
password_cipher:
|
||||
# 留空 private_key_pem 时会在单进程内生成临时密钥;生产多 Pod 请在
|
||||
# geo-rankly-app-secret/config.local.yaml 中配置同一把私钥。
|
||||
enabled: true
|
||||
key_id: login-password-rsa-oaep-v1
|
||||
private_key_pem: ""
|
||||
|
||||
log:
|
||||
level: info,warn,error
|
||||
format: json
|
||||
@@ -182,14 +240,16 @@ browser_fetch:
|
||||
enabled: true
|
||||
provider: lightpanda
|
||||
service_url: http://browser-fetch:8082
|
||||
token: dev-browser-fetch-token
|
||||
queue_size: 64
|
||||
worker_concurrency: 2
|
||||
worker_concurrency: 3
|
||||
request_timeout: 45s
|
||||
cache_ttl: 6h
|
||||
allowed_domains:
|
||||
- zhuanlan.zhihu.com
|
||||
- zhihu.com
|
||||
- baidu.com
|
||||
- weixin.sogou.com
|
||||
trigger_status:
|
||||
- 403
|
||||
- 429
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
server:
|
||||
port: 8090
|
||||
mode: release
|
||||
# 生产环境请配置为真实运维后台域名,例如 https://ops.example.com。
|
||||
# 留空时沿用开发环境 CORS 放开策略。
|
||||
allowed_origins: []
|
||||
security_headers:
|
||||
enabled: true
|
||||
trusted_proxies:
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
@@ -43,6 +48,14 @@ jwt:
|
||||
secret: "geo-ops-15645415841"
|
||||
access_ttl: 8h
|
||||
|
||||
auth:
|
||||
password_cipher:
|
||||
# 留空 private_key_pem 时会在单进程内生成临时密钥;生产多 Pod 请在
|
||||
# geo-rankly-app-secret/ops-config.local.yaml 中配置同一把私钥。
|
||||
enabled: true
|
||||
key_id: ops-password-rsa-oaep-v1
|
||||
private_key_pem: ""
|
||||
|
||||
default_admin:
|
||||
username: admin
|
||||
display_name: system-admin
|
||||
|
||||
@@ -35,3 +35,25 @@ stringData:
|
||||
|
||||
browser_fetch:
|
||||
token: ${BROWSER_FETCH_TOKEN:geo-browser-fetch-change-me}
|
||||
|
||||
auth:
|
||||
password_cipher:
|
||||
enabled: true
|
||||
key_id: login-password-rsa-oaep-v1
|
||||
# 多 Pod 必须使用同一把私钥;上线前用 `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048`
|
||||
# 生成后粘贴完整 PEM。不要把生产私钥提交到代码仓库。
|
||||
private_key_pem: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
REPLACE_WITH_TENANT_LOGIN_PASSWORD_RSA_PRIVATE_KEY
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
ops-config.local.yaml: |
|
||||
auth:
|
||||
password_cipher:
|
||||
enabled: true
|
||||
key_id: ops-password-rsa-oaep-v1
|
||||
# 建议与租户端使用不同私钥;所有 ops-api Pod 必须挂载同一份 Secret。
|
||||
private_key_pem: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
REPLACE_WITH_OPS_LOGIN_PASSWORD_RSA_PRIVATE_KEY
|
||||
-----END PRIVATE KEY-----
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
auth:
|
||||
password_cipher:
|
||||
enabled: true
|
||||
key_id: ops-password-rsa-oaep-v1
|
||||
# 建议与租户端使用不同私钥。复制本文件为 ops-config.local.yaml,
|
||||
# 所有 ops-api 副本必须读取同一份私钥;不要提交生产私钥。
|
||||
# 留空时应用会生成单进程临时私钥,只适合本机单副本试跑。
|
||||
private_key_pem: ""
|
||||
@@ -43,6 +43,13 @@ jwt:
|
||||
secret: "geo-ops-15645415841" # 必须通过 OPS_JWT_SECRET 环境变量提供
|
||||
access_ttl: 8h
|
||||
|
||||
auth:
|
||||
password_cipher:
|
||||
# 生产多实例请在 ops-config.local.yaml 中配置同一把 private_key_pem。
|
||||
enabled: true
|
||||
key_id: ops-password-rsa-oaep-v1
|
||||
private_key_pem: ""
|
||||
|
||||
default_admin:
|
||||
username: admin
|
||||
display_name: 系统管理员
|
||||
|
||||
@@ -25,6 +25,18 @@ if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
echo " 如需填写真实 API Key 或修改端口,可编辑 ${SCRIPT_DIR}/.env 后再次执行 bash deploy.sh"
|
||||
fi
|
||||
|
||||
ensure_local_config() {
|
||||
local file="$1" example="$2"
|
||||
if [[ -f "${SCRIPT_DIR}/${file}" ]]; then
|
||||
return
|
||||
fi
|
||||
if [[ -f "${SCRIPT_DIR}/${example}" ]]; then
|
||||
cp "${SCRIPT_DIR}/${example}" "${SCRIPT_DIR}/${file}"
|
||||
echo "==> 已从 ${example} 创建 ${file}。"
|
||||
echo " 生产多副本部署前请把其中的 password_cipher.private_key_pem 替换为同一把 RSA 私钥。"
|
||||
fi
|
||||
}
|
||||
|
||||
env_value() {
|
||||
local key="$1" default_value="$2" value
|
||||
value="$(grep -E "^${key}=" "${SCRIPT_DIR}/.env" | tail -1 | cut -d= -f2- || true)"
|
||||
@@ -34,6 +46,9 @@ env_value() {
|
||||
}
|
||||
|
||||
deploy_compose() {
|
||||
ensure_local_config "config.local.yaml" "config.local.yaml.example"
|
||||
ensure_local_config "ops-config.local.yaml" "ops-config.local.yaml.example"
|
||||
|
||||
# ─── Step 1: 加载镜像 ───────────────────────────────────────────────────────
|
||||
echo "==> [1/3] 加载 Docker 镜像(可能需要几分钟)..."
|
||||
docker load -i "${SCRIPT_DIR}/images.tar"
|
||||
|
||||
@@ -268,10 +268,12 @@ func RestartRequired(changes []FieldChange) bool {
|
||||
}
|
||||
|
||||
func load(path string) (*Config, error) {
|
||||
resolved := configruntime.New(configruntime.WithSource(
|
||||
runtimefile.NewSource(path),
|
||||
runtimeenv.NewSource("OPS"),
|
||||
))
|
||||
sources := []configruntime.Source{runtimefile.NewSource(path)}
|
||||
if localPath := localOverridePath(path); localPath != "" {
|
||||
sources = append(sources, runtimefile.NewSource(localPath))
|
||||
}
|
||||
sources = append(sources, runtimeenv.NewSource("OPS"))
|
||||
resolved := configruntime.New(configruntime.WithSource(sources...))
|
||||
defer resolved.Close()
|
||||
|
||||
if err := resolved.Load(); err != nil {
|
||||
@@ -571,12 +573,37 @@ func watchFiles(path string) []string {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{path}
|
||||
files := []string{path}
|
||||
if localPath := localOverridePath(path); localPath != "" {
|
||||
files = append(files, localPath)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func watchNames(path string) []string {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
}
|
||||
return []string{filepath.Base(path)}
|
||||
names := []string{filepath.Base(path)}
|
||||
if localPath := localOverridePath(path); localPath != "" {
|
||||
names = append(names, filepath.Base(localPath))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func localOverridePath(path string) string {
|
||||
trimmed := strings.TrimSpace(path)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
ext := filepath.Ext(trimmed)
|
||||
base := strings.TrimSuffix(trimmed, ext)
|
||||
if strings.HasSuffix(base, ".local") {
|
||||
return ""
|
||||
}
|
||||
localPath := base + ".local" + ext
|
||||
if _, err := os.Stat(localPath); err != nil {
|
||||
return ""
|
||||
}
|
||||
return localPath
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ func (c *PasswordCipher) Decrypt(ciphertextB64, keyID string) (string, error) {
|
||||
}
|
||||
|
||||
func parseRSAPrivateKeyPEM(value string) (*rsa.PrivateKey, error) {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
trimmed := normalizePrivateKeyPEM(value)
|
||||
if trimmed == "" {
|
||||
return nil, fmt.Errorf("password cipher private key is required")
|
||||
}
|
||||
@@ -126,3 +126,48 @@ func parseRSAPrivateKeyPEM(value string) (*rsa.PrivateKey, error) {
|
||||
return nil, fmt.Errorf("unsupported private key PEM type %q", block.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizePrivateKeyPEM(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
normalized := strings.ReplaceAll(trimmed, "\r\n", "\n")
|
||||
normalized = strings.ReplaceAll(normalized, "\r", "\n")
|
||||
normalized = strings.ReplaceAll(normalized, `\n`, "\n")
|
||||
if block, _ := pem.Decode([]byte(normalized)); block != nil {
|
||||
return normalized
|
||||
}
|
||||
|
||||
for _, blockType := range []string{"RSA PRIVATE KEY", "PRIVATE KEY"} {
|
||||
begin := "-----BEGIN " + blockType + "-----"
|
||||
end := "-----END " + blockType + "-----"
|
||||
start := strings.Index(normalized, begin)
|
||||
stop := strings.Index(normalized, end)
|
||||
if start < 0 || stop <= start {
|
||||
continue
|
||||
}
|
||||
body := normalized[start+len(begin) : stop]
|
||||
body = strings.Join(strings.Fields(body), "")
|
||||
if body == "" {
|
||||
return normalized
|
||||
}
|
||||
return begin + "\n" + wrapBase64Lines(body, 64) + "\n" + end + "\n"
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
func wrapBase64Lines(value string, width int) string {
|
||||
if width <= 0 || len(value) <= width {
|
||||
return value
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value) + len(value)/width)
|
||||
for len(value) > width {
|
||||
b.WriteString(value[:width])
|
||||
b.WriteByte('\n')
|
||||
value = value[width:]
|
||||
}
|
||||
b.WriteString(value)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -62,3 +63,53 @@ func TestNewEphemeralPasswordCipherExposesPublicKey(t *testing.T) {
|
||||
require.Equal(t, "RSA-OAEP-256", publicKey.Algorithm)
|
||||
require.Contains(t, publicKey.PublicKey, "BEGIN PUBLIC KEY")
|
||||
}
|
||||
|
||||
func TestPasswordCipherAcceptsFoldedPrivateKeyPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
privatePEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: mustMarshalPKCS8PrivateKey(t, key),
|
||||
}))
|
||||
foldedPEM := strings.Join(strings.Fields(privatePEM), " ")
|
||||
|
||||
_, err = NewPasswordCipher(foldedPEM, "folded")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPasswordCipherAcceptsEscapedNewlinePrivateKeyPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
privatePEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: mustMarshalPKCS8PrivateKey(t, key),
|
||||
}))
|
||||
escapedPEM := strings.ReplaceAll(strings.TrimSpace(privatePEM), "\n", `\n`)
|
||||
|
||||
_, err = NewPasswordCipher(escapedPEM, "escaped")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestNormalizePrivateKeyPEMPreservesValidPEM(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
privatePEM := string(pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: mustMarshalPKCS8PrivateKey(t, key),
|
||||
}))
|
||||
|
||||
require.Equal(t, strings.TrimSpace(privatePEM), strings.TrimSpace(normalizePrivateKeyPEM(privatePEM)))
|
||||
}
|
||||
|
||||
func mustMarshalPKCS8PrivateKey(t *testing.T, key *rsa.PrivateKey) []byte {
|
||||
t.Helper()
|
||||
der, err := x509.MarshalPKCS8PrivateKey(key)
|
||||
require.NoError(t, err)
|
||||
return der
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user