Add CRL endpoint, sign-CSR web route, and policy-based issuance authorization

- Register handleSignCSR route in webserver (was dead code)
- Add GET /v1/pki/{mount}/issuer/{name}/crl REST endpoint and
  PKIService.GetCRL gRPC RPC for DER-encoded CRL generation
- Replace admin-only gates on issue/renew/sign-csr with policy-based
  access control: admins grant-all, authenticated users subject to
  identifier ownership (CN/SANs not held by another user's active cert)
  and optional policy overrides via ca/{mount}/id/{identifier} resources
- Add PolicyChecker to engine.Request and policy.Match() method to
  distinguish matched rules from default deny
- Update and expand CA engine tests for ownership, revocation freeing,
  and policy override scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 15:22:04 -07:00
parent fbd6d1af04
commit ac4577f778
11 changed files with 810 additions and 68 deletions

View File

@@ -437,7 +437,38 @@ func TestIssueRejectsNilCallerInfo(t *testing.T) {
}
}
func TestIssueRejectsNonAdmin(t *testing.T) {
func TestIssueAllowsUser(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Users can issue certs for identifiers not held by others.
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "user-cert.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("expected user to issue cert, got: %v", err)
}
if resp.Data["cn"] != "user-cert.example.com" {
t.Errorf("cn: got %v", resp.Data["cn"])
}
}
func TestIssueRejectsGuest(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
@@ -452,7 +483,7 @@ func TestIssueRejectsNonAdmin(t *testing.T) {
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userCaller(),
CallerInfo: guestCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "test.example.com",
@@ -464,7 +495,7 @@ func TestIssueRejectsNonAdmin(t *testing.T) {
}
}
func TestRenewRejectsNonAdmin(t *testing.T) {
func TestIssueRejectsIdentifierInUse(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
@@ -477,9 +508,139 @@ func TestRenewRejectsNonAdmin(t *testing.T) {
t.Fatalf("create-issuer: %v", err)
}
issueResp, err := eng.HandleRequest(ctx, &engine.Request{
// User A issues a cert.
userA := &engine.CallerInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userA,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
"dns_names": []interface{}{"shared.example.com"},
},
})
if err != nil {
t.Fatalf("issue by alice: %v", err)
}
// User B tries to issue for the same CN — should fail.
userB := &engine.CallerInfo{Username: "bob", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userB,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
},
})
if !errors.Is(err, ErrIdentifierInUse) {
t.Errorf("expected ErrIdentifierInUse, got: %v", err)
}
// User A can issue again for the same CN (re-issuance by same user).
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: userA,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("re-issue by alice should succeed: %v", err)
}
// Admin can always issue regardless of ownership.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: adminCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("admin issue should bypass ownership: %v", err)
}
}
func TestIssueRevokedCertFreesIdentifier(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Alice issues a cert.
alice := &engine.CallerInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: alice,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "reclaim.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue: %v", err)
}
serial := resp.Data["serial"].(string) //nolint:errcheck
// Admin revokes it.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "revoke-cert",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"serial": serial},
})
if err != nil {
t.Fatalf("revoke: %v", err)
}
// Bob can now issue for the same CN.
bob := &engine.CallerInfo{Username: "bob", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: bob,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "reclaim.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("bob should be able to issue after revocation: %v", err)
}
}
func TestRenewOwnership(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Alice issues a cert.
alice := &engine.CallerInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
issueResp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: alice,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "test.example.com",
@@ -492,19 +653,49 @@ func TestRenewRejectsNonAdmin(t *testing.T) {
serial := issueResp.Data["serial"].(string) //nolint:errcheck
// Alice can renew her own cert.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"serial": serial,
},
CallerInfo: alice,
Data: map[string]interface{}{"serial": serial},
})
if err != nil {
t.Fatalf("alice should renew her own cert: %v", err)
}
// Bob cannot renew Alice's cert.
bob := &engine.CallerInfo{Username: "bob", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: bob,
Data: map[string]interface{}{"serial": serial},
})
if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden, got: %v", err)
t.Errorf("expected ErrForbidden for bob renewing alice's cert, got: %v", err)
}
// Guest cannot renew.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: guestCaller(),
Data: map[string]interface{}{"serial": serial},
})
if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden for guest, got: %v", err)
}
// Admin can always renew.
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"serial": serial},
})
if err != nil {
t.Fatalf("admin should renew any cert: %v", err)
}
}
func TestSignCSRRejectsNonAdmin(t *testing.T) {
func TestSignCSRRejectsGuest(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
@@ -517,18 +708,6 @@ func TestSignCSRRejectsNonAdmin(t *testing.T) {
t.Fatalf("create-issuer: %v", err)
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "sign-csr",
CallerInfo: userCaller(),
Data: map[string]interface{}{
"issuer": "infra",
"csr_pem": "dummy",
},
})
if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden for user, got: %v", err)
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "sign-csr",
CallerInfo: guestCaller(),
@@ -970,3 +1149,124 @@ func TestPublicMethods(t *testing.T) {
t.Errorf("expected ErrIssuerNotFound, got: %v", err)
}
}
func TestIssuePolicyOverridesOwnership(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Alice issues a cert.
alice := &engine.CallerInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: alice,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue by alice: %v", err)
}
// Bob normally blocked, but policy allows him.
bob := &engine.CallerInfo{Username: "bob", Roles: []string{"user"}, IsAdmin: false}
allowPolicy := func(resource, action string) (string, bool) {
return "allow", true
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: bob,
CheckPolicy: allowPolicy,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "shared.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("bob with allow policy should succeed: %v", err)
}
// Policy deny overrides even for free identifiers.
denyPolicy := func(resource, action string) (string, bool) {
return "deny", true
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: bob,
CheckPolicy: denyPolicy,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "unique-for-bob.example.com",
"profile": "server",
},
})
if !errors.Is(err, ErrForbidden) {
t.Errorf("deny policy should reject: got %v", err)
}
}
func TestRenewPolicyOverridesOwnership(t *testing.T) {
eng, _ := setupEngine(t)
ctx := context.Background()
_, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "create-issuer",
CallerInfo: adminCaller(),
Data: map[string]interface{}{"name": "infra"},
})
if err != nil {
t.Fatalf("create-issuer: %v", err)
}
// Alice issues a cert.
alice := &engine.CallerInfo{Username: "alice", Roles: []string{"user"}, IsAdmin: false}
resp, err := eng.HandleRequest(ctx, &engine.Request{
Operation: "issue",
CallerInfo: alice,
Data: map[string]interface{}{
"issuer": "infra",
"common_name": "policy-renew.example.com",
"profile": "server",
},
})
if err != nil {
t.Fatalf("issue: %v", err)
}
serial := resp.Data["serial"].(string) //nolint:errcheck
// Bob cannot renew without policy.
bob := &engine.CallerInfo{Username: "bob", Roles: []string{"user"}, IsAdmin: false}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: bob,
Data: map[string]interface{}{"serial": serial},
})
if !errors.Is(err, ErrForbidden) {
t.Errorf("expected ErrForbidden, got: %v", err)
}
// Bob with allow policy can renew.
allowPolicy := func(resource, action string) (string, bool) {
return "allow", true
}
_, err = eng.HandleRequest(ctx, &engine.Request{
Operation: "renew",
CallerInfo: bob,
CheckPolicy: allowPolicy,
Data: map[string]interface{}{"serial": serial},
})
if err != nil {
t.Fatalf("bob with policy should renew: %v", err)
}
}