174 lines
8.1 KiB
Go
174 lines
8.1 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/repository"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/secretstore"
|
|
"git.pcux.cn/xuans/Scert/backend/internal/sshkey"
|
|
)
|
|
|
|
func TestSSHKeyAPIHidesPrivateKeyAndBlocksDeletingUsedReference(t *testing.T) {
|
|
directory := t.TempDir()
|
|
cfg := config.Default(directory)
|
|
cfg.Database.DSN = filepath.Join(directory, "data", "ssh-keys.db")
|
|
db, err := database.Open(context.Background(), cfg.Database)
|
|
if err != nil {
|
|
t.Fatalf("打开数据库失败:%v", err)
|
|
}
|
|
defer db.Close()
|
|
if err := database.Migrate(context.Background(), db.DB, "sqlite"); err != nil {
|
|
t.Fatalf("数据库迁移失败:%v", err)
|
|
}
|
|
store, err := secretstore.Open(filepath.Join(directory, "config"))
|
|
if err != nil {
|
|
t.Fatalf("打开敏感信息存储失败:%v", err)
|
|
}
|
|
serverRepo := repository.NewSQLServerRepository(db.DB)
|
|
server := New(Dependencies{
|
|
Config: cfg, Repository: repository.NewSQLDomainRepository(db.DB), Servers: serverRepo,
|
|
SSHKeys: sshkey.NewService(store),
|
|
})
|
|
|
|
privateKey := testSSHPrivateKey(t)
|
|
createBody, err := json.Marshal(sshKeyRequest{Name: "生产私钥", PrivateKey: privateKey})
|
|
if err != nil {
|
|
t.Fatalf("编码 SSH 私钥请求失败:%v", err)
|
|
}
|
|
createRequest := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(createBody)))
|
|
createRequest.Header.Set("Content-Type", "application/json")
|
|
createResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(createResponse, createRequest)
|
|
if createResponse.Code != http.StatusCreated {
|
|
t.Fatalf("创建 SSH 私钥失败:%d %s", createResponse.Code, createResponse.Body.String())
|
|
}
|
|
if strings.Contains(createResponse.Body.String(), "PRIVATE KEY") || strings.Contains(createResponse.Body.String(), "private_key") {
|
|
t.Fatalf("SSH 私钥创建响应泄露了敏感内容:%s", createResponse.Body.String())
|
|
}
|
|
var key sshkey.Metadata
|
|
if err := json.Unmarshal(createResponse.Body.Bytes(), &key); err != nil || key.ID == "" || key.Reference == "" {
|
|
t.Fatalf("SSH 私钥创建响应无效:%v %+v", err, key)
|
|
}
|
|
|
|
listResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(listResponse, httptest.NewRequest(http.MethodGet, "/api/v1/ssh-keys", nil))
|
|
if listResponse.Code != http.StatusOK || !strings.Contains(listResponse.Body.String(), key.Reference) || strings.Contains(listResponse.Body.String(), "PRIVATE KEY") {
|
|
t.Fatalf("SSH 私钥列表响应无效或泄露敏感内容:%d %s", listResponse.Code, listResponse.Body.String())
|
|
}
|
|
|
|
serverBody := `{"name":"生产服务器","address":"192.0.2.60","port":22,"username":"deploy","private_key_ref":"` + key.Reference + `","host_key_fingerprint":"SHA256:test","certificate_path":"/etc/scert/cert.pem","private_key_path":"/etc/scert/key.pem","certificate_chain_path":"/etc/scert/fullchain.pem","web_server":"nginx"}`
|
|
serverRequest := httptest.NewRequest(http.MethodPost, "/api/v1/servers", strings.NewReader(serverBody))
|
|
serverRequest.Header.Set("Content-Type", "application/json")
|
|
serverResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(serverResponse, serverRequest)
|
|
if serverResponse.Code != http.StatusCreated {
|
|
t.Fatalf("创建使用 SSH 私钥的服务器失败:%d %s", serverResponse.Code, serverResponse.Body.String())
|
|
}
|
|
var createdServer struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(serverResponse.Body.Bytes(), &createdServer); err != nil || createdServer.ID == "" {
|
|
t.Fatalf("服务器创建响应无效:%v %s", err, serverResponse.Body.String())
|
|
}
|
|
|
|
deleteUsedResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(deleteUsedResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
|
if deleteUsedResponse.Code != http.StatusConflict {
|
|
t.Fatalf("仍被使用的 SSH 私钥应禁止删除:%d %s", deleteUsedResponse.Code, deleteUsedResponse.Body.String())
|
|
}
|
|
deleteServerResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(deleteServerResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/servers/"+createdServer.ID, nil))
|
|
if deleteServerResponse.Code != http.StatusNoContent {
|
|
t.Fatalf("删除测试服务器失败:%d %s", deleteServerResponse.Code, deleteServerResponse.Body.String())
|
|
}
|
|
deleteKeyResponse := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(deleteKeyResponse, httptest.NewRequest(http.MethodDelete, "/api/v1/ssh-keys/"+key.ID, nil))
|
|
if deleteKeyResponse.Code != http.StatusNoContent {
|
|
t.Fatalf("删除未使用的 SSH 私钥失败:%d %s", deleteKeyResponse.Code, deleteKeyResponse.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSSHKeyAPIRejectsInvalidKeyWithoutEchoingIt(t *testing.T) {
|
|
store, err := secretstore.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("打开敏感信息存储失败:%v", err)
|
|
}
|
|
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
|
secret := "invalid-private-key-sensitive-content"
|
|
body, _ := json.Marshal(sshKeyRequest{Name: "错误私钥", PrivateKey: secret})
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(body)))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(response, request)
|
|
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), secret) {
|
|
t.Fatalf("无效 SSH 私钥响应不安全:%d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSSHKeyAPIRejectsOversizedRequestBody(t *testing.T) {
|
|
store, err := secretstore.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("failed to open secret store: %v", err)
|
|
}
|
|
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
|
body := `{"name":"oversized request","private_key":"` + strings.Repeat("a", maxSSHKeyRequestBytes) + `"}`
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(body))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(response, request)
|
|
if response.Code != http.StatusRequestEntityTooLarge {
|
|
t.Fatalf("oversized SSH key request should return 413: %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSSHKeyAPIRejectsOversizedPrivateKeyAndPassphrase(t *testing.T) {
|
|
store, err := secretstore.Open(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("failed to open secret store: %v", err)
|
|
}
|
|
server := New(Dependencies{Config: config.Default(t.TempDir()), Repository: &stubDomainRepository{}, SSHKeys: sshkey.NewService(store)})
|
|
requests := []sshKeyRequest{
|
|
{Name: "oversized key", PrivateKey: strings.Repeat("a", sshkey.MaxPrivateKeyBytes+1)},
|
|
{Name: "oversized passphrase", PrivateKey: testSSHPrivateKey(t), Passphrase: strings.Repeat("p", 4097)},
|
|
}
|
|
for _, input := range requests {
|
|
body, err := json.Marshal(input)
|
|
if err != nil {
|
|
t.Fatalf("failed to encode limit test request: %v", err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/ssh-keys", strings.NewReader(string(body)))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
server.Handler().ServeHTTP(response, request)
|
|
leaksPassphrase := input.Passphrase != "" && strings.Contains(response.Body.String(), input.Passphrase)
|
|
if response.Code != http.StatusBadRequest || strings.Contains(response.Body.String(), input.PrivateKey) || leaksPassphrase {
|
|
t.Fatalf("oversized input should be rejected safely: %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func testSSHPrivateKey(t *testing.T) string {
|
|
t.Helper()
|
|
_, key, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatalf("生成测试 SSH 私钥失败:%v", err)
|
|
}
|
|
encoded, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
t.Fatalf("编码测试 SSH 私钥失败:%v", err)
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: encoded}))
|
|
}
|