Files
certctl/internal/api/handler/scep_handler_test.go
T
shankar0123 bcefb11e65 feat(M51): add SCEP server (RFC 8894) for MDM and network device enrollment
Implements Simple Certificate Enrollment Protocol with single-endpoint
operation-based dispatch (GetCACaps, GetCACert, PKIOperation), PKCS#7
SignedData CSR extraction with fallback for raw/base64 CSR, challenge
password authentication via CSR attributes, and shared internal/pkcs7
package extracted from EST handler to eliminate code duplication.

24 new tests (11 service + 13 handler) plus 5 shared pkcs7 package tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 16:47:18 -04:00

263 lines
6.7 KiB
Go

package handler
import (
"context"
"encoding/pem"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/shankar0123/certctl/internal/domain"
)
// mockSCEPService implements SCEPService for testing.
type mockSCEPService struct {
CACaps string
CACertPEM string
CACertErr error
EnrollResult *domain.SCEPEnrollResult
EnrollErr error
}
func (m *mockSCEPService) GetCACaps(ctx context.Context) string {
if m.CACaps != "" {
return m.CACaps
}
return "POSTPKIOperation\nSHA-256\nAES\nSCEPStandard\n"
}
func (m *mockSCEPService) GetCACert(ctx context.Context) (string, error) {
return m.CACertPEM, m.CACertErr
}
func (m *mockSCEPService) PKCSReq(ctx context.Context, csrPEM string, challengePassword string, transactionID string) (*domain.SCEPEnrollResult, error) {
return m.EnrollResult, m.EnrollErr
}
func TestSCEP_GetCACaps_Success(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACaps", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
ct := w.Header().Get("Content-Type")
if ct != "text/plain" {
t.Errorf("expected text/plain, got %s", ct)
}
body := w.Body.String()
if !strings.Contains(body, "POSTPKIOperation") {
t.Errorf("expected POSTPKIOperation in response, got: %s", body)
}
if !strings.Contains(body, "SHA-256") {
t.Errorf("expected SHA-256 in response, got: %s", body)
}
}
func TestSCEP_GetCACaps_MethodNotAllowed(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/scep?operation=GetCACaps", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", w.Code)
}
}
func TestSCEP_GetCACert_Success_SingleCert(t *testing.T) {
certPEM := generateTestCertPEM(t)
svc := &mockSCEPService{
CACertPEM: certPEM,
}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACert", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
ct := w.Header().Get("Content-Type")
if ct != "application/x-x509-ca-cert" {
t.Errorf("expected application/x-x509-ca-cert, got %s", ct)
}
if w.Body.Len() == 0 {
t.Error("expected non-empty body")
}
}
func TestSCEP_GetCACert_MethodNotAllowed(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/scep?operation=GetCACert", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", w.Code)
}
}
func TestSCEP_GetCACert_ServiceError(t *testing.T) {
svc := &mockSCEPService{
CACertErr: errors.New("CA unavailable"),
}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep?operation=GetCACert", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected 500, got %d", w.Code)
}
}
func TestSCEP_PKIOperation_MethodNotAllowed(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep?operation=PKIOperation", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusMethodNotAllowed {
t.Errorf("expected 405, got %d", w.Code)
}
}
func TestSCEP_PKIOperation_EmptyBody(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(""))
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestSCEP_PKIOperation_InvalidBody(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader("not-valid-asn1-or-csr"))
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestSCEP_PKIOperation_ServiceError(t *testing.T) {
svc := &mockSCEPService{
EnrollErr: errors.New("enrollment failed"),
}
h := NewSCEPHandler(svc)
// Generate a valid raw CSR DER to send as body (fallback path)
csrPEM := generateTestCSRPEM(t)
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
t.Fatal("failed to decode CSR PEM")
}
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String())
}
}
func TestSCEP_PKIOperation_Success_RawCSR(t *testing.T) {
certPEM := generateTestCertPEM(t)
svc := &mockSCEPService{
EnrollResult: &domain.SCEPEnrollResult{
CertPEM: certPEM,
ChainPEM: "",
},
}
h := NewSCEPHandler(svc)
csrPEM := generateTestCSRPEM(t)
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
t.Fatal("failed to decode CSR PEM")
}
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
}
ct := w.Header().Get("Content-Type")
if ct != "application/x-pki-message" {
t.Errorf("expected application/x-pki-message, got %s", ct)
}
}
func TestSCEP_PKIOperation_ChallengePasswordRejected(t *testing.T) {
svc := &mockSCEPService{
EnrollErr: errors.New("invalid challenge password"),
}
h := NewSCEPHandler(svc)
csrPEM := generateTestCSRPEM(t)
block, _ := pem.Decode([]byte(csrPEM))
if block == nil {
t.Fatal("failed to decode CSR PEM")
}
req := httptest.NewRequest(http.MethodPost, "/scep?operation=PKIOperation", strings.NewReader(string(block.Bytes)))
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d: %s", w.Code, w.Body.String())
}
}
func TestSCEP_UnknownOperation(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep?operation=UnknownOp", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestSCEP_MissingOperation(t *testing.T) {
svc := &mockSCEPService{}
h := NewSCEPHandler(svc)
req := httptest.NewRequest(http.MethodGet, "/scep", nil)
w := httptest.NewRecorder()
h.HandleSCEP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}