Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
87d506f
feat(servercomm): add secure HTTP transport
BenCodez Sep 5, 2026
2951868
fix(servercomm): persist HTTP replay state
BenCodez Sep 5, 2026
f62bf69
fix(servercomm): confirm HTTP delivery acknowledgements
BenCodez Sep 5, 2026
e9159c1
fix(servercomm): fail closed on partial HTTP state
BenCodez Sep 5, 2026
e3f08cc
fix(servercomm): bound authority state and support IPv6
BenCodez Sep 5, 2026
4b92ea3
fix(servercomm): bound proxy state and secure credential roots
BenCodez Sep 5, 2026
1faa2d2
fix(servercomm): separate HTTP acknowledgement directions
BenCodez Sep 5, 2026
c01f2cc
Handle HTTP renewal and backend state churn
BenCodez Sep 5, 2026
80a55da
Make HTTP revocation persistence retryable
BenCodez Sep 5, 2026
9d0166f
Fix remaining HTTP transport review findings
BenCodez Sep 5, 2026
ea26fde
Track uncertain HTTP queue publications
BenCodez Sep 5, 2026
8d8e9c8
Make HTTP enrollment and queue retries safe
BenCodez Sep 5, 2026
989ec3f
Fix remaining HTTP transport review findings
BenCodez Sep 5, 2026
831f2f1
Fix transport queue uncertainty and 429 handling
Copilot Sep 6, 2026
6f31643
Keep uncertain queue entries hidden after restart
BenCodez Sep 6, 2026
a6967d8
Recover generated sends and published credential replacements
BenCodez Sep 6, 2026
cf155d9
Recover authority and completed delivery persistence failures
BenCodez Sep 6, 2026
5bcce2d
Retry pre-callback state writes and bound quarantined backends
BenCodez Sep 6, 2026
11130ed
Enforce verified private permissions for HTTP transport state
BenCodez Sep 6, 2026
99ebcc6
Confirm credential activation and safely finish callback shutdown
BenCodez Sep 6, 2026
4c7887f
Preserve enrollment CA trust and validate codes before reservation
BenCodez Sep 6, 2026
c8befc4
Require trusted CA key continuity for credential renewal
BenCodez Sep 6, 2026
2ab7908
Enforce one-second minimum enrollment lifetime
BenCodez Sep 6, 2026
d6a202c
Lock inbound journal ownership and validate client key pairs
BenCodez Sep 6, 2026
c9b9e6f
Recover journal retirement and lock outgoing proxy queues
BenCodez Sep 6, 2026
d5e8474
Prevent backend state creation after proxy shutdown
BenCodez Sep 6, 2026
2fe4343
Close backend send admission and enforce pending enrollment expiry
BenCodez Sep 6, 2026
d2754e9
Rate limit certificate renewal and serialize TLS identity writes
BenCodez Sep 6, 2026
8a994f0
Reload persisted TLS identity before renewal
BenCodez Sep 6, 2026
7bf06f9
Restore inbound journals within the backend capacity bound
BenCodez Sep 6, 2026
c390c0f
test: isolate failed HTTP revocation retries
BenCodez Sep 6, 2026
7beae58
Preserve failed revocation target across retries
BenCodez Sep 6, 2026
826e680
Wait for concurrent JSON saves in tests
BenCodez Sep 6, 2026
af984b8
Recover durable HTTP setup failure paths
BenCodez Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion SimpleAPI/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.release>21</maven.compiler.release>
<bouncycastle.version>1.85</bouncycastle.version>
</properties>
<build>
<sourceDirectory>src/main/java</sourceDirectory>
Expand Down Expand Up @@ -71,6 +72,11 @@
<finalName>${project.name}</finalName>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
Expand Down Expand Up @@ -155,6 +161,16 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
Expand Down Expand Up @@ -407,4 +423,4 @@
</build>
</profile>
</profiles>
</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.bencodez.simpleapi.file;

import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Locale;

/** Cross-platform helpers for forcing file contents and published directory entries. */
public final class DurableFiles {
private DurableFiles() { }

public static void forceFile(Path file) throws IOException {
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
channel.force(true);
}
}

public static void forceDirectory(Path directory) throws IOException {
if (directory == null) return;
try {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
channel.force(true);
}
} catch (AccessDeniedException unsupportedDirectoryHandle) {
// The Windows NIO provider cannot open directory handles. File contents are
// still forced before atomic publication; do not make persistence unusable.
if (!isWindowsName(System.getProperty("os.name", ""))) throw unsupportedDirectoryHandle;
} catch (UnsupportedOperationException unsupportedDirectoryForce) {
// Some providers support atomic moves but expose no directory-force operation.
}
}

public static boolean deleteIfExists(Path target) throws IOException {
boolean deleted = Files.deleteIfExists(target);
if (deleted) forceDirectory(target.toAbsolutePath().normalize().getParent());
return deleted;
}

public static void forceMoveDirectories(Path source, Path target) throws IOException {
try {
Path sourceParent = source.toAbsolutePath().normalize().getParent();
Path targetParent = target.toAbsolutePath().normalize().getParent();
forceDirectory(targetParent);
if (sourceParent != null && !sourceParent.equals(targetParent)) forceDirectory(sourceParent);
} catch (IOException failure) {
throw new PublishedException(failure);
}
}

public static boolean isWindowsName(String name) {
return name != null && name.trim().toLowerCase(Locale.ROOT).startsWith("windows");
}

/** Indicates that an atomic rename completed before metadata writeback failed. */
@SuppressWarnings("serial")
public static final class PublishedException extends IOException {
public PublishedException(IOException cause) {
super("File was published but its directory metadata could not be forced", cause);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.bencodez.simpleapi.file;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.AclEntry;
import java.nio.file.attribute.AclEntryPermission;
import java.nio.file.attribute.AclEntryType;
import java.nio.file.attribute.AclFileAttributeView;
import java.nio.file.attribute.GroupPrincipal;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.UserPrincipal;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

/** Enforces and verifies owner-only access for persisted private material. */
public final class PrivateFilePermissions {
private static final Set<PosixFilePermission> OWNER_FILE = EnumSet.of(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
private static final Set<PosixFilePermission> OWNER_DIRECTORY = EnumSet.of(
PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
private static final Set<AclEntryPermission> OWNER_ACL = EnumSet.allOf(AclEntryPermission.class);

private PrivateFilePermissions() { }

/** Enforces 0600-equivalent access, or fails when the provider cannot prove it. */
public static void ownerOnlyFile(Path path) throws IOException {
enforce(path, OWNER_FILE);
}

/** Enforces 0700-equivalent access, or fails when the provider cannot prove it. */
public static void ownerOnlyDirectory(Path path) throws IOException {
enforce(path, OWNER_DIRECTORY);
}

private static void enforce(Path path, Set<PosixFilePermission> permissions) throws IOException {
if (path == null) throw new IllegalArgumentException("Private path is required");
if (Files.isSymbolicLink(path)) throw new IOException("Refusing symbolic link for private storage: " + path);
try {
Files.setPosixFilePermissions(path, permissions);
if (!Files.getPosixFilePermissions(path, LinkOption.NOFOLLOW_LINKS).equals(permissions))
throw new IOException("Could not verify owner-only POSIX permissions for " + path);
return;
} catch (UnsupportedOperationException unsupported) {
// A non-POSIX provider must expose an ACL that can be reduced and verified.
}
enforceWithAcl(path);
}

private static void enforceWithAcl(Path path) throws IOException {
try {
AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
if (view == null) throw new IOException("Owner-only private storage is unsupported for " + path);
enforceWithAcl(view, path);
} catch (UnsupportedOperationException unsupported) {
throw new IOException("Owner-only private storage is unsupported for " + path, unsupported);
}
}

/** Package-visible for deterministic ACL-provider tests. */
static void enforceWithAcl(AclFileAttributeView view, Path path) throws IOException {
try {
if (view == null) throw new IOException("Owner-only private storage is unsupported for " + path);
UserPrincipal owner = view.getOwner();
if (owner == null || owner instanceof GroupPrincipal)
throw new IOException("Private storage owner is not an individual account for " + path);
AclEntry ownerEntry = AclEntry.newBuilder().setType(AclEntryType.ALLOW).setPrincipal(owner)
.setPermissions(OWNER_ACL).build();
view.setAcl(List.of(ownerEntry));
List<AclEntry> acl = view.getAcl();
if (acl.size() != 1 || !isVerifiedOwnerEntry(acl.get(0), owner))
throw new IOException("Could not verify owner-only ACL permissions for " + path);
} catch (UnsupportedOperationException unsupported) {
throw new IOException("Owner-only private storage is unsupported for " + path, unsupported);
}
}

private static boolean isVerifiedOwnerEntry(AclEntry entry, UserPrincipal owner) {
return entry.type() == AclEntryType.ALLOW && owner.equals(entry.principal()) && entry.flags().isEmpty()
&& entry.permissions().equals(OWNER_ACL);
}
}
Loading
Loading