Add directory-bound ACME accounts, orders, authorizations, challenge evidence, strict JWS processing, issuance, rollover and revocation. Isolate bounded ACME execution from administrative and public services while preserving explicit authority, profile, issuer and chain-path selection.
367 lines
22 KiB
Java
367 lines
22 KiB
Java
/*******************************************************************************
|
|
* Copyright (C) 2026, Leo Galambos
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without modification,
|
|
* are permitted provided that the following conditions are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
* list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
* this list of conditions and the following disclaimer in the documentation
|
|
* and/or other materials provided with the distribution.
|
|
*
|
|
* 3. All advertising materials mentioning features or use of this software must
|
|
* display the following acknowledgement:
|
|
* This product includes software developed by the Egothor project.
|
|
*
|
|
* 4. Neither the name of the copyright holder nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software without
|
|
* specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
|
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
******************************************************************************/
|
|
package zeroecho.pki.server;
|
|
|
|
import java.time.Instant;
|
|
import java.util.Objects;
|
|
import java.util.Optional;
|
|
|
|
import zeroecho.pki.api.PkiId;
|
|
import zeroecho.pki.server.acme.AcmeService;
|
|
|
|
/** Closed transport-neutral server-control administration operation hierarchy. */
|
|
@SuppressWarnings("PMD.ControlStatementBraces")
|
|
public sealed interface ServerControlOperation permits ServerControlOperation.RegisterPrincipal,
|
|
ServerControlOperation.InspectPrincipal, ServerControlOperation.ListPrincipals,
|
|
ServerControlOperation.SetPrincipalEnabled, ServerControlOperation.ListRoleTemplates,
|
|
ServerControlOperation.InspectRoleTemplate, ServerControlOperation.CreateRoleAssignment,
|
|
ServerControlOperation.InspectRoleAssignment, ServerControlOperation.ListRoleAssignments,
|
|
ServerControlOperation.RevokeRoleAssignment, ServerControlOperation.CreateGrant,
|
|
ServerControlOperation.InspectGrant, ServerControlOperation.ListGrants,
|
|
ServerControlOperation.RevokeGrant, ServerControlOperation.EvaluateAuthorization,
|
|
ServerControlOperation.RequestApproval, ServerControlOperation.InspectApproval,
|
|
ServerControlOperation.ListApprovals, ServerControlOperation.DecideApproval,
|
|
ServerControlOperation.CancelApproval, ServerControlOperation.CreateBreakGlass,
|
|
ServerControlOperation.ActivateBreakGlass,
|
|
ServerControlOperation.InspectBreakGlass, ServerControlOperation.ListBreakGlass,
|
|
ServerControlOperation.RevokeBreakGlass, ServerControlOperation.InspectDisclosure,
|
|
ServerControlOperation.SetDisclosure, ServerControlOperation.IssueCapability,
|
|
ServerControlOperation.RevokeCapability, ServerControlOperation.InspectAuditView,
|
|
ServerControlOperation.InspectRepositoryAlias, ServerControlOperation.ListRepositoryAliases,
|
|
ServerControlOperation.SetRepositoryAlias, ServerControlOperation.RemoveRepositoryAlias,
|
|
ServerControlOperation.RegisterAcmeDirectory, ServerControlOperation.InspectAcmeDirectory,
|
|
ServerControlOperation.ListAcmeDirectories, ServerControlOperation.SetAcmeDirectoryActive,
|
|
ServerControlOperation.InspectAcmeAccount, ServerControlOperation.ListAcmeAccounts,
|
|
ServerControlOperation.DeactivateAcmeAccount {
|
|
|
|
/** @return stable operation identity */
|
|
String name();
|
|
|
|
/** Principal registration. */
|
|
record RegisterPrincipal(SecurityPrincipal principal) implements ServerControlOperation {
|
|
public static final String NAME = "security.principal.register";
|
|
public RegisterPrincipal { Objects.requireNonNull(principal, "principal"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Principal lookup. */
|
|
record InspectPrincipal(String principalId) implements ServerControlOperation {
|
|
public static final String NAME = "security.principal.inspect";
|
|
public InspectPrincipal { Permission.requirePrincipal(principalId); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Bounded principal page. */
|
|
record ListPrincipals(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "security.principal.list";
|
|
public ListPrincipals { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Principal enablement transition. */
|
|
record SetPrincipalEnabled(String principalId, boolean enabled) implements ServerControlOperation {
|
|
public static final String ENABLE = "security.principal.enable";
|
|
public static final String DISABLE = "security.principal.disable";
|
|
public SetPrincipalEnabled { Permission.requirePrincipal(principalId); }
|
|
@Override public String name() { return enabled ? ENABLE : DISABLE; }
|
|
}
|
|
/** Immutable role-template list. */
|
|
record ListRoleTemplates() implements ServerControlOperation {
|
|
public static final String NAME = "security.role-template.list";
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Immutable role-template lookup. */
|
|
record InspectRoleTemplate(String templateId, int version) implements ServerControlOperation {
|
|
public static final String NAME = "security.role-template.inspect";
|
|
public InspectRoleTemplate { Permission.requireId(templateId, "role template"); if (version <= 0) throw invalid(); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Scoped assignment creation. */
|
|
record CreateRoleAssignment(RoleTemplateCatalog.Assignment assignment) implements ServerControlOperation {
|
|
public static final String NAME = "security.role-assignment.create";
|
|
public CreateRoleAssignment { Objects.requireNonNull(assignment, "assignment"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Scoped assignment lookup. */
|
|
record InspectRoleAssignment(String assignmentId) implements ServerControlOperation {
|
|
public static final String NAME = "security.role-assignment.inspect";
|
|
public InspectRoleAssignment { Permission.requireId(assignmentId, "assignment"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Bounded assignment page. */
|
|
record ListRoleAssignments(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "security.role-assignment.list";
|
|
public ListRoleAssignments { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Durable assignment revocation. */
|
|
record RevokeRoleAssignment(String assignmentId) implements ServerControlOperation {
|
|
public static final String NAME = "security.role-assignment.revoke";
|
|
public RevokeRoleAssignment { Permission.requireId(assignmentId, "assignment"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Direct grant creation. */
|
|
record CreateGrant(Permission.Grant grant) implements ServerControlOperation {
|
|
public static final String NAME = "security.grant.create";
|
|
public CreateGrant { Objects.requireNonNull(grant, "grant"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Direct grant lookup. */
|
|
record InspectGrant(String grantId) implements ServerControlOperation {
|
|
public static final String NAME = "security.grant.inspect";
|
|
public InspectGrant { Permission.requireId(grantId, "grant"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Bounded direct-grant page. */
|
|
record ListGrants(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "security.grant.list";
|
|
public ListGrants { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Durable direct-grant revocation. */
|
|
record RevokeGrant(String grantId) implements ServerControlOperation {
|
|
public static final String NAME = "security.grant.revoke";
|
|
public RevokeGrant { Permission.requireId(grantId, "grant"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Side-effect-free authorization simulation. */
|
|
record EvaluateAuthorization(String principalId, Permission.Action action, Permission.Resource resource,
|
|
Permission.Relationship relationship, Permission.DataView dataView,
|
|
Permission.Context context) implements ServerControlOperation {
|
|
public static final String NAME = "security.authorization.evaluate";
|
|
public EvaluateAuthorization { Permission.requirePrincipal(principalId); Objects.requireNonNull(action);
|
|
Objects.requireNonNull(resource); Objects.requireNonNull(relationship); Objects.requireNonNull(dataView);
|
|
Objects.requireNonNull(context); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Exact future-operation approval request. */
|
|
record RequestApproval(String approvalId, AdministrativeOperation targetOperation,
|
|
Permission.Resource targetResource) implements ServerControlOperation {
|
|
public static final String NAME = "security.approval.request";
|
|
public RequestApproval { Permission.requireId(approvalId, "approval");
|
|
Objects.requireNonNull(targetOperation); Objects.requireNonNull(targetResource);
|
|
if (targetOperation instanceof AdministrativeOperation.Control control
|
|
&& control.operation() instanceof RequestApproval) throw invalid(); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Approval lookup. */
|
|
record InspectApproval(String approvalId) implements ServerControlOperation {
|
|
public static final String NAME = "security.approval.inspect";
|
|
public InspectApproval { Permission.requireId(approvalId, "approval"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Bounded approval page. */
|
|
record ListApprovals(int offset, int limit, Optional<ApprovalService.State> state,
|
|
Optional<String> requesterPrincipalId, Optional<String> targetOperationId)
|
|
implements ServerControlOperation {
|
|
public static final String NAME = "security.approval.list";
|
|
public ListApprovals { page(offset, limit); state = Objects.requireNonNull(state);
|
|
requesterPrincipalId = Objects.requireNonNull(requesterPrincipalId)
|
|
.map(Permission::requirePrincipal);
|
|
targetOperationId = Objects.requireNonNull(targetOperationId)
|
|
.map(value -> Permission.requireBounded(value, 256, "target operation")); }
|
|
/** Creates an unfiltered bounded approval page. */
|
|
public ListApprovals(int offset, int limit) {
|
|
this(offset, limit, Optional.empty(), Optional.empty(), Optional.empty());
|
|
}
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Approval or rejection decision. */
|
|
record DecideApproval(String approvalId, ApprovalService.Choice choice,
|
|
String justification) implements ServerControlOperation {
|
|
public static final String APPROVE = "security.approval.approve";
|
|
public static final String REJECT = "security.approval.reject";
|
|
public DecideApproval { Permission.requireId(approvalId, "approval"); Objects.requireNonNull(choice);
|
|
Permission.requireBounded(justification, 2048, "justification"); }
|
|
@Override public String name() { return choice == ApprovalService.Choice.APPROVE ? APPROVE : REJECT; }
|
|
}
|
|
/** Approval cancellation. */
|
|
record CancelApproval(String approvalId) implements ServerControlOperation {
|
|
public static final String NAME = "security.approval.cancel";
|
|
public CancelApproval { Permission.requireId(approvalId, "approval"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Independently approved emergency grant creation. */
|
|
record CreateBreakGlass(String breakGlassId, String principalId, Permission.Grant grant,
|
|
String reason, Instant expiresAt) implements ServerControlOperation {
|
|
public static final String CREATE = "security.break-glass.create";
|
|
public CreateBreakGlass { Permission.requireId(breakGlassId, "break-glass");
|
|
Permission.requirePrincipal(principalId); Objects.requireNonNull(grant);
|
|
Permission.requireBounded(reason, 2048, "reason"); Objects.requireNonNull(expiresAt); }
|
|
@Override public String name() { return CREATE; }
|
|
}
|
|
/** Activation of one already-created approved emergency grant. */
|
|
record ActivateBreakGlass(String breakGlassId) implements ServerControlOperation {
|
|
public static final String ACTIVATE = "security.break-glass.activate";
|
|
public ActivateBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
|
|
@Override public String name() { return ACTIVATE; }
|
|
}
|
|
/** Break-glass lookup. */
|
|
record InspectBreakGlass(String breakGlassId) implements ServerControlOperation {
|
|
public static final String NAME = "security.break-glass.inspect";
|
|
public InspectBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Bounded break-glass page. */
|
|
record ListBreakGlass(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "security.break-glass.list";
|
|
public ListBreakGlass { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Break-glass revocation. */
|
|
record RevokeBreakGlass(String breakGlassId) implements ServerControlOperation {
|
|
public static final String NAME = "security.break-glass.revoke";
|
|
public RevokeBreakGlass { Permission.requireId(breakGlassId, "break-glass"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Disclosure metadata lookup. */
|
|
record InspectDisclosure(PkiId objectId) implements ServerControlOperation {
|
|
public static final String NAME = "security.disclosure.inspect";
|
|
public InspectDisclosure { Objects.requireNonNull(objectId); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Disclosure-only state transition. */
|
|
record SetDisclosure(PkiId objectId, DisclosureService.Policy policy) implements ServerControlOperation {
|
|
public static final String NAME = "security.disclosure.set";
|
|
public SetDisclosure { Objects.requireNonNull(objectId); Objects.requireNonNull(policy); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** One-time unlisted retrieval capability issuance. */
|
|
record IssueCapability(PkiId objectId, Instant expiresAt) implements ServerControlOperation {
|
|
public static final String NAME = "security.disclosure.capability.issue";
|
|
public IssueCapability { Objects.requireNonNull(objectId); Objects.requireNonNull(expiresAt); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Capability revocation by non-bearer record identity. */
|
|
record RevokeCapability(String capabilityId) implements ServerControlOperation {
|
|
public static final String NAME = "security.disclosure.capability.revoke";
|
|
public RevokeCapability { Permission.requireId(capabilityId, "capability"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Existing safe auditor projection over one server-control object. */
|
|
record InspectAuditView(PkiId objectId, Permission.DataView view,
|
|
Optional<String> reasonReference) implements ServerControlOperation {
|
|
public static final String REDACTED = "security.audit-view.inspect-redacted";
|
|
public static final String FULL = "security.audit-view.inspect-full";
|
|
public static final String PII = "security.audit-view.inspect-pii";
|
|
public InspectAuditView { Objects.requireNonNull(objectId); Objects.requireNonNull(view);
|
|
reasonReference = Objects.requireNonNull(reasonReference); }
|
|
@Override public String name() { return switch (view) { case METADATA_REDACTED -> REDACTED;
|
|
case METADATA_FULL, CONTENT_FULL -> FULL; case PII_FULL -> PII; }; }
|
|
}
|
|
|
|
/** Exact repository alias inspection. */
|
|
record InspectRepositoryAlias(PkiId authorityId, RepositoryAliasService.Type type)
|
|
implements ServerControlOperation {
|
|
public static final String NAME = "repository.alias.inspect";
|
|
public InspectRepositoryAlias { Objects.requireNonNull(authorityId); Objects.requireNonNull(type); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
|
|
/** Bounded repository alias page. */
|
|
record ListRepositoryAliases(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "repository.alias.list";
|
|
public ListRepositoryAliases { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
|
|
/** Exact CRL or chain alias publication with expected-current protection. */
|
|
record SetRepositoryAlias(PkiId authorityId, RepositoryAliasService.Type type, PkiId targetObjectId,
|
|
Optional<PkiId> issuerId, Optional<String> expectedCurrentCommitment) implements ServerControlOperation {
|
|
public static final String NAME = "repository.alias.set";
|
|
public SetRepositoryAlias {
|
|
Objects.requireNonNull(authorityId); Objects.requireNonNull(type); Objects.requireNonNull(targetObjectId);
|
|
issuerId = Objects.requireNonNull(issuerId); expectedCurrentCommitment = Objects.requireNonNull(expectedCurrentCommitment);
|
|
}
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
|
|
/** Durable repository alias removal without target mutation. */
|
|
record RemoveRepositoryAlias(PkiId authorityId, RepositoryAliasService.Type type,
|
|
String expectedCurrentCommitment) implements ServerControlOperation {
|
|
public static final String NAME = "repository.alias.remove";
|
|
public RemoveRepositoryAlias { Objects.requireNonNull(authorityId); Objects.requireNonNull(type);
|
|
if (expectedCurrentCommitment == null || !expectedCurrentCommitment.matches("[0-9a-f]{64}")) throw invalid(); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
|
|
/** Registers one immutable inactive ACME directory revision. */
|
|
record RegisterAcmeDirectory(AcmeService.DirectoryRegistration registration) implements ServerControlOperation {
|
|
public static final String NAME = "acme.directory.register";
|
|
public RegisterAcmeDirectory { Objects.requireNonNull(registration, "registration"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Inspects one exact ACME directory revision. */
|
|
record InspectAcmeDirectory(String directoryId) implements ServerControlOperation {
|
|
public static final String NAME = "acme.directory.inspect";
|
|
public InspectAcmeDirectory { Permission.requireId(directoryId, "ACME directory"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Lists a bounded page of ACME directory revisions. */
|
|
record ListAcmeDirectories(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "acme.directory.list";
|
|
public ListAcmeDirectories { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Activates or deactivates one exact ACME directory revision. */
|
|
record SetAcmeDirectoryActive(String directoryId, boolean active) implements ServerControlOperation {
|
|
public static final String ACTIVATE = "acme.directory.activate";
|
|
public static final String DEACTIVATE = "acme.directory.deactivate";
|
|
public SetAcmeDirectoryActive { Permission.requireId(directoryId, "ACME directory"); }
|
|
@Override public String name() { return active ? ACTIVATE : DEACTIVATE; }
|
|
}
|
|
/** Inspects one protocol-scoped ACME account. */
|
|
record InspectAcmeAccount(String accountId) implements ServerControlOperation {
|
|
public static final String NAME = "acme.account.inspect";
|
|
public InspectAcmeAccount { Permission.requireId(accountId, "ACME account"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Lists a bounded account page without contact PII. */
|
|
record ListAcmeAccounts(int offset, int limit) implements ServerControlOperation {
|
|
public static final String NAME = "acme.account.list";
|
|
public ListAcmeAccounts { page(offset, limit); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
/** Durably deactivates one ACME account without deleting history. */
|
|
record DeactivateAcmeAccount(String accountId) implements ServerControlOperation {
|
|
public static final String NAME = "acme.account.deactivate";
|
|
public DeactivateAcmeAccount { Permission.requireId(accountId, "ACME account"); }
|
|
@Override public String name() { return NAME; }
|
|
}
|
|
|
|
private static void page(int offset, int limit) {
|
|
if (offset < 0 || limit <= 0 || limit > 256) throw invalid();
|
|
}
|
|
private static IllegalArgumentException invalid() {
|
|
return new IllegalArgumentException("Server-control operation input is invalid");
|
|
}
|
|
}
|