8 Commits

Author SHA1 Message Date
2333b01a3f fix(build): update Foojay toolchain resolver for Gradle 9
All checks were successful
Release / release (push) Successful in 1m16s
2026-06-03 11:46:07 +02:00
de55ea909f Integrate CredentialFrameworkProvider bootstrap SPI and harden provider config validation
feat: add SPI-based CredentialFrameworkProvider resolution to PkiBootstrap via ServiceLoader
feat: add PkiBootstrap.openCredentialFramework() for provider-driven credential framework initialization
feat: register BcX509CredentialFrameworkProvider in META-INF/services
feat: introduce ConfigurableProvider.validateConfig(ProviderConfig) as a standard provider-side validation hook
fix: move generic backendId consistency validation into the default ConfigurableProvider validation routine
fix: enforce provider-local configuration validation from allocate() so direct provider use remains safe outside bootstrap
fix: add provider-specific validateConfig implementations for bootstrap-managed providers based on consumed configuration keys
fix: report unknown provider configuration keys through provider-local JUL warning logs without exposing values
fix: fail fast on malformed consumed configuration values instead of silently falling back where invalid input would mask operator error
fix: extend PkiBootstrapTest to cover CredentialFrameworkProvider bootstrap path
fix: extend PkiBootstrapTest to cover async and crypto.workflow initialization paths whose prefixed properties are cleared in test setup
fix: add negative bootstrap/provider validation coverage for backend mismatch and invalid configured values
docs: expand JavaDoc and package-level documentation for CredentialFrameworkProvider bootstrap wiring, ServiceLoader usage, and configuration validation behavior
chore: keep PkiBootstrap independent from implementation-specific BC framework classes and preserve provider autonomy over validation and diagnostics

Closes #3 spent @2h
2026-04-06 01:51:15 +02:00
a66c115a80 chore: removal of the obsolete mockito-inline:5.2.0 2026-04-05 23:15:09 +02:00
e74e833c5b chore: extract shared classic-leg wiring in HybridKexBuilder
Extract duplicated classic-leg construction from
HybridKexBuilder.buildInitiator() and buildResponder()
into a private buildClassicLeg() helper with JavaDoc.

This keeps classic mode validation and context creation
in one place, reduces asymmetry risk between initiator
and responder paths, and preserves existing behavior.

Closes #18 spent @30m
2026-04-05 22:56:47 +02:00
14fbf31989 fix: replace CryptoAlgorithms audit wrap instanceof chain with Java 21
switch

Replace the AUDIT_MODE == WRAP dispatch in
zeroecho.core.CryptoAlgorithms#create(...) with an exhaustive Java 21
pattern switch over the sealed CryptoContext hierarchy. This removes the
repeated instanceof chain, keeps unchecked casts localized in a single
internal helper, and closes the missing audit-wrap gap for
AgreementContext.

Add focused JUnit 5 coverage for audited proxy wrapping using
Mockito-based tests for representative context interfaces and wrapper
lifecycle delegation.

Closes #20
Time-Spent: 45m
2026-04-05 22:17:14 +02:00
a4b9eeffe1 chore: restore canonical LICENSE filename 2026-04-01 23:09:44 +02:00
e235d0e2b5 chore: rename license temporarily 2026-04-01 23:09:26 +02:00
d1bdf7d9df Split integrations and export into ext module
feat: move integrations from lib to ext
feat: move content export from lib to ext
feat: rename affected packages for separate module distribution
chore: update Gradle module wiring
chore: adjust JPMS descriptors and dependencies
docs: update module structure documentation
2026-04-01 20:43:10 +02:00
50 changed files with 1381 additions and 171 deletions

View File

@@ -9,8 +9,9 @@ dependencies {
implementation 'org.apache.commons:commons-text' implementation 'org.apache.commons:commons-text'
implementation 'commons-cli:commons-cli' implementation 'commons-cli:commons-cli'
implementation project(':lib') implementation project(':lib')
// might be removed if I move BC ops to the lib implementation project(':ext')
testImplementation 'org.bouncycastle:bcpkix-jdk18on' // might be removed if I move BC ops to the lib
testImplementation 'org.bouncycastle:bcpkix-jdk18on'
} }
application { application {

View File

@@ -51,8 +51,8 @@ import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options; import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException; import org.apache.commons.cli.ParseException;
import zeroecho.sdk.integrations.covert.jpeg.JpegExifEmbedder; import zeroecho.ext.integrations.covert.jpeg.JpegExifEmbedder;
import zeroecho.sdk.integrations.covert.jpeg.Slot; import zeroecho.ext.integrations.covert.jpeg.Slot;
/** /**
* Command-line extension of ZeroEcho for covert embedding and extraction of * Command-line extension of ZeroEcho for covert embedding and extraction of

32
ext/.classpath Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
ext/.project Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ext</name>
<comment>Project ext created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

31
ext/LICENSE Normal file
View File

@@ -0,0 +1,31 @@
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.

13
ext/build.gradle Normal file
View File

@@ -0,0 +1,13 @@
plugins {
id 'buildlogic.java-library-conventions'
id 'com.palantir.git-version'
}
group='org.egothor'
dependencies {
api 'org.bouncycastle:bcpkix-jdk18on'
implementation 'org.apache.commons:commons-imaging'
implementation project(':lib')
testImplementation 'org.egothor:conflux'
}

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.content.export; package zeroecho.ext.content.export;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.content.export; package zeroecho.ext.content.export;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;

View File

@@ -36,10 +36,10 @@
* *
* <p> * <p>
* This package provides streaming utilities and exportable content * This package provides streaming utilities and exportable content
* implementations that render {@link zeroecho.sdk.content.api.DataContent} for * implementations that render {@link zeroecho.ext.content.api.DataContent} for
* deployment to external platforms or for script-based transport. Exports can * deployment to external platforms or for script-based transport. Exports can
* be produced as raw bytes or as platform-specific scripts according to * be produced as raw bytes or as platform-specific scripts according to
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}. * {@link zeroecho.ext.content.api.ExportableDataContent.ExportMode}.
* </p> * </p>
* *
* <h2>Scope</h2> * <h2>Scope</h2>
@@ -60,15 +60,15 @@
* <li><i>Piwigo uploader</i> - an exportable content implementation * <li><i>Piwigo uploader</i> - an exportable content implementation
* (package-private) that can either upload an image directly to a Piwigo server * (package-private) that can either upload an image directly to a Piwigo server
* or generate Bash/CMD scripts that reconstruct and upload the image. It is * or generate Bash/CMD scripts that reconstruct and upload the image. It is
* built on {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and * built on {@link zeroecho.ext.content.api.AbstractExportableDataContent} and
* honors * honors
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li> * {@link zeroecho.ext.content.api.ExportableDataContent.ExportMode}.</li>
* </ul> * </ul>
* *
* <h2>Typical usage</h2> * <h2>Typical usage</h2>
* <h3>Format a stream as Base64 command lines</h3> <pre>{@code * <h3>Format a stream as Base64 command lines</h3> <pre>{@code
* java.io.InputStream source = ... raw bytes ...; * java.io.InputStream source = ... raw bytes ...;
* java.io.InputStream encoded = new zeroecho.sdk.content.export.Base64Stream( * java.io.InputStream encoded = new zeroecho.ext.content.export.Base64Stream(
* source, * source,
* "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8), * "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8),
* 76, * 76,
@@ -78,8 +78,8 @@
* }</pre> * }</pre>
* *
* <h3>Render an exportable content in a chosen mode</h3> <pre>{@code * <h3>Render an exportable content in a chosen mode</h3> <pre>{@code
* zeroecho.sdk.content.api.ExportableDataContent content = ... some exportable content ...; * zeroecho.ext.content.api.ExportableDataContent content = ... some exportable content ...;
* content.setExportMode(zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT); * content.setExportMode(zeroecho.ext.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
* try (java.io.InputStream script = content.getStream()) { * try (java.io.InputStream script = content.getStream()) {
* script.transferTo(out); * script.transferTo(out);
* } * }
@@ -87,7 +87,7 @@
* *
* <h2>Security notes</h2> * <h2>Security notes</h2>
* <ul> * <ul>
* <li>Prefer exporting {@link zeroecho.sdk.content.api.EncryptedContent} when * <li>Prefer exporting {@link zeroecho.ext.content.api.EncryptedContent} when
* targeting untrusted destinations.</li> * targeting untrusted destinations.</li>
* <li>Avoid embedding secrets in scripts; pass credentials via environment * <li>Avoid embedding secrets in scripts; pass credentials via environment
* variables or secure stores when possible.</li> * variables or secure stores when possible.</li>
@@ -98,8 +98,8 @@
* <h2>Extensibility</h2> * <h2>Extensibility</h2>
* <ul> * <ul>
* <li>New deployers should extend * <li>New deployers should extend
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and select a * {@link zeroecho.ext.content.api.AbstractExportableDataContent} and select a
* default {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode} * default {@link zeroecho.ext.content.api.ExportableDataContent.ExportMode}
* appropriate for the platform.</li> * appropriate for the platform.</li>
* <li>Utilities like {@link Base64Stream} can be reused to generate * <li>Utilities like {@link Base64Stream} can be reused to generate
* platform-friendly payloads without buffering whole files.</li> * platform-friendly payloads without buffering whole files.</li>
@@ -107,4 +107,4 @@
* *
* @since 1.0 * @since 1.0
*/ */
package zeroecho.sdk.content.export; package zeroecho.ext.content.export;

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Content abstractions and streaming payloads for the SDK.
*
* <p>
* This package defines the common content model used by the SDK and organizes
* concrete sources and exporters under dedicated subpackages. Content objects
* expose streaming access to bytes and can be chained into pipelines produced
* by builder APIs.
* </p>
*
* <h2>Subpackages</h2>
* <ul>
* <li>{@link zeroecho.ext.content.export} - export helpers and platform
* deployers, such as {@link zeroecho.ext.content.export.Base64Stream} and
* exportable content that targets external destinations (for example, gallery
* platforms) or script-based transports.</li>
* </ul>
*
* <h2>Responsibilities</h2>
* <ul>
* <li>Support exportable content that can render itself as raw bytes or
* platform-specific scripts via
* {@link zeroecho.sdk.content.api.ExportableDataContent}.</li>
* </ul>
*
* <h2>Typical usage</h2> <pre>{@code
* // Stream a DataContent instance to an OutputStream.
* zeroecho.sdk.content.api.DataContent content = ... obtained from a builder chain ...;
* try (java.io.InputStream in = content.getStream()) {
* in.transferTo(out);
* }
*
* // If exportable, render in an alternate mode (for example, a shell script).
* if (content instanceof zeroecho.sdk.content.api.ExportableDataContent exportable) {
* exportable.setExportMode(
* zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
* try (java.io.InputStream script = exportable.getStream()) {
* script.transferTo(out);
* }
* }
* }</pre>
*
* <h2>Extensibility</h2>
* <ul>
* <li>New exporters should extend
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and honor the
* selected
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li>
* <li>The export area is intended to host deployers for additional public
* platforms and, where appropriate, steganographic carriers for concealed
* delivery of encrypted streams.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.ext.content;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.covert; package zeroecho.ext.integrations.covert;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.Map; import java.util.Map;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg; package zeroecho.ext.integrations.covert.jpeg;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg; package zeroecho.ext.integrations.covert.jpeg;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;

View File

@@ -110,4 +110,4 @@
* *
* @since 1.0 * @since 1.0
*/ */
package zeroecho.sdk.integrations.covert.jpeg; package zeroecho.ext.integrations.covert.jpeg;

View File

@@ -54,8 +54,8 @@
* *
* <h2>Typical usage</h2> <pre>{@code * <h2>Typical usage</h2> <pre>{@code
* // Use the built-in English distribution to create a short cover text. * // Use the built-in English distribution to create a short cover text.
* zeroecho.sdk.integrations.covert.TextualCodec.Generator gen = * zeroecho.ext.integrations.covert.TextualCodec.Generator gen =
* zeroecho.sdk.integrations.covert.TextualCodec.Generator.EN; * zeroecho.ext.integrations.covert.TextualCodec.Generator.EN;
* String sample = gen.getText(256); * String sample = gen.getText(256);
* *
* // Or construct a custom distribution. * // Or construct a custom distribution.
@@ -63,8 +63,8 @@
* java.util.Map.entry('e', 12.7), java.util.Map.entry('t', 9.1), * java.util.Map.entry('e', 12.7), java.util.Map.entry('t', 9.1),
* java.util.Map.entry('a', 8.2), java.util.Map.entry(' ', 25.4) * java.util.Map.entry('a', 8.2), java.util.Map.entry(' ', 25.4)
* ); * );
* zeroecho.sdk.integrations.covert.TextualCodec.Generator custom = * zeroecho.ext.integrations.covert.TextualCodec.Generator custom =
* new zeroecho.sdk.integrations.covert.TextualCodec.Generator(freq); * new zeroecho.ext.integrations.covert.TextualCodec.Generator(freq);
* String cover = custom.getText(128); * String cover = custom.getText(128);
* }</pre> * }</pre>
* *
@@ -78,4 +78,4 @@
* *
* @since 1.0 * @since 1.0
*/ */
package zeroecho.sdk.integrations.covert; package zeroecho.ext.integrations.covert;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;
/** /**
* Enumeration of image formats supported for steganographic processing. * Enumeration of image formats supported for steganographic processing.

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;
/** /**
* Metadata that describes a steganographic method. * Metadata that describes a steganographic method.

View File

@@ -44,11 +44,11 @@
* <h2>Design principles</h2> * <h2>Design principles</h2>
* <ul> * <ul>
* <li>All algorithms implement the * <li>All algorithms implement the
* {@link zeroecho.sdk.integrations.stegano.SteganographyMethod} interface, * {@link zeroecho.ext.integrations.stegano.SteganographyMethod} interface,
* which defines stream-oriented {@code embed} and {@code extract} operations * which defines stream-oriented {@code embed} and {@code extract} operations
* and supplies a metadata descriptor.</li> * and supplies a metadata descriptor.</li>
* <li>Supported carrier formats are represented by * <li>Supported carrier formats are represented by
* {@link zeroecho.sdk.integrations.stegano.ImageFormat}. Only lossless formats * {@link zeroecho.ext.integrations.stegano.ImageFormat}. Only lossless formats
* are suitable for direct bit-level embedding, but some algorithms may also * are suitable for direct bit-level embedding, but some algorithms may also
* provide support for lossy domains such as JPEG with dedicated * provide support for lossy domains such as JPEG with dedicated
* techniques.</li> * techniques.</li>
@@ -64,7 +64,7 @@
* the frequency domain (for example, DCT coefficients for JPEG) or more * the frequency domain (for example, DCT coefficients for JPEG) or more
* advanced hybrid techniques. New methods can be registered by implementing the * advanced hybrid techniques. New methods can be registered by implementing the
* {@code SteganographyMethod} interface and returning appropriate * {@code SteganographyMethod} interface and returning appropriate
* {@link zeroecho.sdk.integrations.stegano.StegoMetadata}. * {@link zeroecho.ext.integrations.stegano.StegoMetadata}.
* </p> * </p>
* *
* <h2>Typical workflow</h2> <pre>{@code * <h2>Typical workflow</h2> <pre>{@code
@@ -81,4 +81,4 @@
* } * }
* }</pre> * }</pre>
*/ */
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.content.export; package zeroecho.ext.content.export;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.covert; package zeroecho.ext.integrations.covert;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg; package zeroecho.ext.integrations.covert.jpeg;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;

View File

@@ -31,7 +31,7 @@
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/ ******************************************************************************/
package zeroecho.sdk.integrations.stegano; package zeroecho.ext.integrations.stegano;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;

View File

Before

Width:  |  Height:  |  Size: 230 KiB

After

Width:  |  Height:  |  Size: 230 KiB

View File

@@ -13,13 +13,6 @@
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources"> <classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes> <attributes>
<attribute name="gradle_scope" value="main"/> <attribute name="gradle_scope" value="main"/>

View File

@@ -5,10 +5,19 @@ plugins {
group='org.egothor' group='org.egothor'
configurations {
mockitoAgent
}
dependencies { dependencies {
api 'org.bouncycastle:bcpkix-jdk18on' api 'org.bouncycastle:bcpkix-jdk18on'
implementation 'org.egothor:conflux' implementation 'org.egothor:conflux'
implementation 'org.apache.commons:commons-imaging'
testImplementation "org.mockito:mockito-core:5.23.0"
mockitoAgent("org.mockito:mockito-core:5.23.0") {
transitive = false
}
} }
@@ -54,3 +63,8 @@ javadoc {
options.links("https://www.egothor.org/javadoc/conflux") options.links("https://www.egothor.org/javadoc/conflux")
// options.overview = file("src/main/javadoc/overview.html") // options.overview = file("src/main/javadoc/overview.html")
} }
test {
useJUnitPlatform()
jvmArgs("-javaagent:${configurations.mockitoAgent.singleFile}")
}

View File

@@ -49,6 +49,7 @@ import javax.crypto.SecretKey;
import zeroecho.core.audit.AuditListener; import zeroecho.core.audit.AuditListener;
import zeroecho.core.audit.AuditedContexts; import zeroecho.core.audit.AuditedContexts;
import zeroecho.core.context.AgreementContext;
import zeroecho.core.context.CryptoContext; import zeroecho.core.context.CryptoContext;
import zeroecho.core.context.DigestContext; import zeroecho.core.context.DigestContext;
import zeroecho.core.context.EncryptionContext; import zeroecho.core.context.EncryptionContext;
@@ -335,33 +336,39 @@ public final class CryptoAlgorithms {
if (AUDIT_MODE == AuditMode.WRAP) { if (AUDIT_MODE == AuditMode.WRAP) {
final AuditListener listener = AUDIT; // pass through the global listener final AuditListener listener = AUDIT; // pass through the global listener
if (ctx instanceof SignatureContext) { return switch (ctx) {
@SuppressWarnings("unchecked") case SignatureContext signatureContext -> wrapForAudit(signatureContext, listener, role);
C out = (C) AuditedContexts.wrap(ctx, listener, role); case EncryptionContext encryptionContext -> wrapForAudit(encryptionContext, listener, role);
return out; case KemContext kemContext -> wrapForAudit(kemContext, listener, role);
} else if (ctx instanceof EncryptionContext) { case DigestContext digestContext -> wrapForAudit(digestContext, listener, role);
@SuppressWarnings("unchecked") case MacContext macContext -> wrapForAudit(macContext, listener, role);
C out = (C) AuditedContexts.wrap(ctx, listener, role); case AgreementContext agreementContext -> wrapForAudit(agreementContext, listener, role);
return out; };
} else if (ctx instanceof KemContext) {
@SuppressWarnings("unchecked")
C out = (C) AuditedContexts.wrap(ctx, listener, role);
return out;
} else if (ctx instanceof DigestContext) {
@SuppressWarnings("unchecked")
C out = (C) AuditedContexts.wrap(ctx, listener, role);
return out;
} else if (ctx instanceof MacContext) {
@SuppressWarnings("unchecked")
C out = (C) AuditedContexts.wrap(ctx, listener, role);
return out;
}
// Unknown context type: return as-is (no wrapping).
} }
return ctx; return ctx;
} }
/**
* Returns the audited wrapper for the supplied context.
*
* <p>
* The returned context remains owned by the caller of the factory method. This
* helper does not acquire an additional resource requiring local cleanup.
* </p>
*
* @param <C> context type
* @param context source context
* @param listener audit listener
* @param role key usage role
* @return audited wrapper
*/
@SuppressWarnings("unchecked")
/* default */ static <C extends CryptoContext> C wrapForAudit(CryptoContext context, AuditListener listener,
KeyUsage role) {
return (C) AuditedContexts.wrap(context, listener, role);
}
/** /**
* Creates a {@link CryptoContext} using the algorithms default spec for the * Creates a {@link CryptoContext} using the algorithms default spec for the
* role. * role.

View File

@@ -232,6 +232,43 @@ public final class HybridKexBuilder {
return new PqcKem(this); return new PqcKem(this);
} }
/**
* Builds the configured classic agreement leg for the current builder state.
*
* <p>
* This method validates the classic-leg inputs required by the selected
* {@link ClassicMode} and returns the resulting {@link AgreementContext} ready
* for inclusion into a {@link HybridKexContext}. For
* {@link ClassicMode#CLASSIC_AGREEMENT}, the returned context is also bound to
* the configured peer public key.
* </p>
*
* @return classic agreement context derived from the configured classic-leg
* state
* @throws IOException if underlying context creation fails
* @throws IllegalStateException if the selected classic mode is missing
* required state
*/
private AgreementContext buildClassicLeg() throws IOException {
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
if (classicPrivate == null || classicPeerPublic == null) {
throw new IllegalStateException(
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
}
AgreementContext classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate,
classicSpec);
classic.setPeerPublic(classicPeerPublic);
return classic;
}
if (classicMode == ClassicMode.PAIR_MESSAGE) {
if (classicKeyPair == null) {
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
}
return CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
}
throw new IllegalStateException("classic mode must be selected");
}
/** /**
* Builds initiator-side context. * Builds initiator-side context.
* *
@@ -241,22 +278,7 @@ public final class HybridKexBuilder {
public HybridKexContext buildInitiator() throws IOException { public HybridKexContext buildInitiator() throws IOException {
validateCommon(); validateCommon();
AgreementContext classic; AgreementContext classic = buildClassicLeg();
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
if (classicPrivate == null || classicPeerPublic == null) {
throw new IllegalStateException(
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
}
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, classicSpec);
classic.setPeerPublic(classicPeerPublic);
} else if (classicMode == ClassicMode.PAIR_MESSAGE) {
if (classicKeyPair == null) {
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
}
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
} else {
throw new IllegalStateException("classic mode must be selected");
}
if (pqcPeerPublic == null) { if (pqcPeerPublic == null) {
throw new IllegalStateException("pqc peer public must be set for initiator"); throw new IllegalStateException("pqc peer public must be set for initiator");
@@ -279,22 +301,7 @@ public final class HybridKexBuilder {
public HybridKexContext buildResponder() throws IOException { public HybridKexContext buildResponder() throws IOException {
validateCommon(); validateCommon();
AgreementContext classic; AgreementContext classic = buildClassicLeg();
if (classicMode == ClassicMode.CLASSIC_AGREEMENT) {
if (classicPrivate == null || classicPeerPublic == null) {
throw new IllegalStateException(
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
}
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicPrivate, classicSpec);
classic.setPeerPublic(classicPeerPublic);
} else if (classicMode == ClassicMode.PAIR_MESSAGE) {
if (classicKeyPair == null) {
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");
}
classic = CryptoAlgorithms.create(classicAlgId, KeyUsage.AGREEMENT, classicKeyPair, classicSpec);
} else {
throw new IllegalStateException("classic mode must be selected");
}
if (pqcPrivate == null) { if (pqcPrivate == null) {
throw new IllegalStateException("pqc private key must be set for responder"); throw new IllegalStateException("pqc private key must be set for responder");

View File

@@ -54,10 +54,6 @@
* {@link zeroecho.sdk.content.builtin.PlainString}, * {@link zeroecho.sdk.content.builtin.PlainString},
* {@link zeroecho.sdk.content.builtin.PlainFile}, and * {@link zeroecho.sdk.content.builtin.PlainFile}, and
* {@link zeroecho.sdk.content.builtin.SecretPassword}.</li> * {@link zeroecho.sdk.content.builtin.SecretPassword}.</li>
* <li>{@link zeroecho.sdk.content.export} - export helpers and platform
* deployers, such as {@link zeroecho.sdk.content.export.Base64Stream} and
* exportable content that targets external destinations (for example, gallery
* platforms) or script-based transports.</li>
* </ul> * </ul>
* *
* <h2>Responsibilities</h2> * <h2>Responsibilities</h2>

View File

@@ -0,0 +1,111 @@
/*******************************************************************************
* 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.core;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import java.lang.reflect.Proxy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms.AuditMode;
import zeroecho.core.audit.AuditListener;
import zeroecho.core.context.AgreementContext;
import zeroecho.core.context.CryptoContext;
import zeroecho.core.context.DigestContext;
/**
* Verifies audited proxy wrapping for representative {@link CryptoContext}
* contract types.
*
* <p>
* These tests focus on the internal audit wrapping path used by
* {@link CryptoAlgorithms#wrapForAudit(CryptoContext, zeroecho.core.audit.AuditListener, KeyUsage)}.
* They verify that representative context types are wrapped as audited JDK
* proxies and that the resulting wrapper preserves the expected basic
* delegation behavior.
* </p>
*/
class CryptoAlgorithmsAuditWrapTest {
@AfterEach
void restoreAuditConfiguration() {
CryptoAlgorithms.setAuditListener(AuditListener.noop());
CryptoAlgorithms.setAuditMode(AuditMode.OFF);
}
@Test
void wrapForAuditDigestContextReturnsProxy() {
System.out.println("wrapForAuditDigestContextReturnsProxy");
DigestContext context = mock(DigestContext.class);
DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST);
System.out.println("...ctxClass=" + wrapped.getClass().getName());
assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Digest context should be wrapped as JDK proxy");
System.out.println("wrapForAuditDigestContextReturnsProxy...ok");
}
@Test
void wrapForAuditAgreementContextReturnsProxy() {
System.out.println("wrapForAuditAgreementContextReturnsProxy");
AgreementContext context = mock(AgreementContext.class);
AgreementContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.AGREEMENT);
System.out.println("...ctxClass=" + wrapped.getClass().getName());
assertTrue(Proxy.isProxyClass(wrapped.getClass()), "Agreement context should be wrapped as JDK proxy");
System.out.println("wrapForAuditAgreementContextReturnsProxy...ok");
}
@Test
void wrapForAuditDigestContextCloseDelegatesToWrappedContext() throws Exception {
System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext");
DigestContext context = mock(DigestContext.class);
DigestContext wrapped = CryptoAlgorithms.wrapForAudit(context, AuditListener.noop(), KeyUsage.DIGEST);
wrapped.close();
verify(context).close();
System.out.println("...wrappedCloseDelegated=true");
System.out.println("wrapForAuditDigestContextCloseDelegatesToWrappedContext...ok");
}
}

View File

@@ -0,0 +1,471 @@
/*******************************************************************************
* 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.sdk.builders;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.security.KeyPair;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.common.agreement.KeyPairKey;
import zeroecho.core.alg.kyber.KyberKeyGenSpec;
import zeroecho.core.alg.xdh.XdhSpec;
import zeroecho.sdk.hybrid.kex.HybridKexContext;
import zeroecho.sdk.hybrid.kex.HybridKexPolicy;
import zeroecho.sdk.hybrid.kex.HybridKexProfile;
import zeroecho.sdk.hybrid.kex.HybridKexTranscript;
import zeroecho.sdk.util.BouncyCastleActivator;
/**
* Professional unit and regression tests for {@link HybridKexBuilder}.
*
* <p>
* These tests verify both supported classic-leg construction modes,
* builder-side validation failures, policy enforcement, transcript binding, and
* mode-switch behavior. The builder is exercised strictly through its public
* fluent API so that validation and resulting hybrid context construction are
* covered together.
* </p>
*/
class HybridKexBuilderTest {
@BeforeAll
static void setup() {
try {
BouncyCastleActivator.init();
} catch (Throwable ignore) {
// Keep tests runnable even when BC is not present.
}
}
@Test
void buildInitiatorResponderClassicAgreementRoundTrip() throws Exception {
System.out.println("buildInitiatorResponderClassicAgreementRoundTrip");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
HybridKexTranscript transcript = new HybridKexTranscript().addUtf8("suite", "X25519+ML-KEM-768").addUtf8("role",
"builder-test");
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
HybridKexContext alice = null;
HybridKexContext bob = null;
try {
alice = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement()
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate())
.peerPublic(bobClassic.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
.buildInitiator();
bob = HybridKexBuilder.builder().profile(profile).transcript(transcript).classicAgreement().algorithm("Xdh")
.spec(XdhSpec.X25519).privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic())
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
byte[] aliceMessage = alice.getPeerMessage();
System.out.println("...aliceMessage(" + lens(aliceMessage) + ")=" + hex(aliceMessage));
bob.setPeerMessage(aliceMessage);
byte[] secretAlice = alice.deriveSecret();
byte[] secretBob = bob.deriveSecret();
System.out.println("...secretAlice=" + hex(secretAlice));
System.out.println("...secretBob=" + hex(secretBob));
assertNotNull(secretAlice);
assertNotNull(secretBob);
assertArrayEquals(secretAlice, secretBob);
assertEquals(profile.outLenBytes(), secretAlice.length);
} finally {
closeQuietly(alice);
closeQuietly(bob);
}
System.out.println("buildInitiatorResponderClassicAgreementRoundTrip...ok");
}
@Test
void buildInitiatorResponderPairMessageRoundTrip() throws Exception {
System.out.println("buildInitiatorResponderPairMessageRoundTrip");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
HybridKexContext alice = null;
HybridKexContext bob = null;
try {
alice = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh")
.spec(XdhSpec.X25519).keyPair(new KeyPairKey(aliceClassic)).pqcKem().algorithm("ML-KEM")
.peerPublic(bobPqc.getPublic()).buildInitiator();
bob = HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
.keyPair(new KeyPairKey(bobClassic)).pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate())
.buildResponder();
byte[] messageA = alice.getPeerMessage();
System.out.println("...messageA(" + lens(messageA) + ")=" + hex(messageA));
bob.setPeerMessage(messageA);
byte[] messageB = bob.getPeerMessage();
System.out.println("...messageB(" + lens(messageB) + ")=" + hex(messageB));
alice.setPeerMessage(messageB);
byte[] secretAlice = alice.deriveSecret();
byte[] secretBob = bob.deriveSecret();
System.out.println("...secretAlice=" + hex(secretAlice));
System.out.println("...secretBob=" + hex(secretBob));
assertNotNull(secretAlice);
assertNotNull(secretBob);
assertArrayEquals(secretAlice, secretBob);
assertEquals(profile.outLenBytes(), secretAlice.length);
} finally {
closeQuietly(alice);
closeQuietly(bob);
}
System.out.println("buildInitiatorResponderPairMessageRoundTrip...ok");
}
@Test
void buildInitiatorWithoutProfileFails() throws Exception {
System.out.println("buildInitiatorWithoutProfileFails");
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
.algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("profile must be set", exception.getMessage());
System.out.println("buildInitiatorWithoutProfileFails...ok");
}
@Test
void buildInitiatorWithoutClassicModeFails() throws Exception {
System.out.println("buildInitiatorWithoutClassicModeFails");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().profile(profile).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
.buildInitiator();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("classic mode must be selected", exception.getMessage());
System.out.println("buildInitiatorWithoutClassicModeFails...ok");
}
@Test
void buildInitiatorClassicAgreementWithoutPeerPublicFails() throws Exception {
System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
.privateKey(aliceClassic.getPrivate()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic())
.buildInitiator();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("classic private key and peer public must be set for CLASSIC_AGREEMENT", exception.getMessage());
System.out.println("buildInitiatorClassicAgreementWithoutPeerPublicFails...ok");
}
@Test
void buildResponderPairMessageWithoutKeyPairFails() throws Exception {
System.out.println("buildResponderPairMessageWithoutKeyPairFails");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().profile(profile).classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
.pqcKem().algorithm("ML-KEM").privateKey(bobPqc.getPrivate()).buildResponder();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("classic key pair must be set for PAIR_MESSAGE", exception.getMessage());
System.out.println("buildResponderPairMessageWithoutKeyPairFails...ok");
}
@Test
void buildInitiatorWithoutPqcPeerPublicFails() throws Exception {
System.out.println("buildInitiatorWithoutPqcPeerPublicFails");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
.privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic()).pqcKem()
.algorithm("ML-KEM").buildInitiator();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("pqc peer public must be set for initiator", exception.getMessage());
System.out.println("buildInitiatorWithoutPqcPeerPublicFails...ok");
}
@Test
void buildResponderWithoutPqcPrivateFails() throws Exception {
System.out.println("buildResponderWithoutPqcPrivateFails");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> {
HybridKexBuilder.builder().profile(profile).classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519)
.privateKey(bobClassic.getPrivate()).peerPublic(aliceClassic.getPublic()).pqcKem()
.algorithm("ML-KEM").buildResponder();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("pqc private key must be set for responder", exception.getMessage());
System.out.println("buildResponderWithoutPqcPrivateFails...ok");
}
@Test
void buildInitiatorRejectsPolicyWhenOkmTooShort() throws Exception {
System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort");
HybridKexProfile profile = HybridKexProfile.defaultProfile(16);
HybridKexPolicy policy = new HybridKexPolicy(0, 0, 32);
KeyPair aliceClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassic = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
HybridKexBuilder.builder().profile(profile).policy(policy).classicAgreement().algorithm("Xdh")
.spec(XdhSpec.X25519).privateKey(aliceClassic.getPrivate()).peerPublic(bobClassic.getPublic())
.pqcKem().algorithm("ML-KEM").peerPublic(bobPqc.getPublic()).buildInitiator();
});
System.out.println("...exception=" + exception.getMessage());
assertEquals("Hybrid OKM length too small: 16 < 32", exception.getMessage());
System.out.println("buildInitiatorRejectsPolicyWhenOkmTooShort...ok");
}
@Test
void switchingClassicModeClearsConflictingStateAndBuildsPairMessage() throws Exception {
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair agreementKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair pairMessageKeyPair = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqc = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
HybridKexContext context = null;
try {
HybridKexBuilder builder = HybridKexBuilder.builder().profile(profile);
builder.classicAgreement().algorithm("Xdh").spec(XdhSpec.X25519).privateKey(agreementKeyPair.getPrivate())
.peerPublic(agreementKeyPair.getPublic());
context = builder.classicPairMessage().algorithm("Xdh").spec(XdhSpec.X25519)
.keyPair(new KeyPairKey(pairMessageKeyPair)).pqcKem().algorithm("ML-KEM")
.peerPublic(bobPqc.getPublic()).buildInitiator();
byte[] peerMessage = context.getPeerMessage();
System.out.println("...peerMessage(" + lens(peerMessage) + ")=" + hex(peerMessage));
assertNotNull(context);
assertNotNull(peerMessage);
} finally {
closeQuietly(context);
}
System.out.println("switchingClassicModeClearsConflictingStateAndBuildsPairMessage...ok");
}
@Test
void transcriptChangesDerivedSecret() throws Exception {
System.out.println("transcriptChangesDerivedSecret");
HybridKexProfile profile = HybridKexProfile.defaultProfile(32);
KeyPair aliceClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassicA = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqcA = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
KeyPair aliceClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobClassicB = CryptoAlgorithms.generateKeyPair("Xdh", XdhSpec.X25519);
KeyPair bobPqcB = CryptoAlgorithms.generateKeyPair("ML-KEM", KyberKeyGenSpec.kyber768());
HybridKexTranscript transcriptA = new HybridKexTranscript().addUtf8("context", "A");
HybridKexTranscript transcriptB = new HybridKexTranscript().addUtf8("context", "B");
byte[] secretA;
byte[] secretB;
HybridKexContext aliceA = null;
HybridKexContext bobA = null;
HybridKexContext aliceB = null;
HybridKexContext bobB = null;
try {
aliceA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicA.getPrivate())
.peerPublic(bobClassicA.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcA.getPublic())
.buildInitiator();
bobA = HybridKexBuilder.builder().profile(profile).transcript(transcriptA).classicAgreement()
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicA.getPrivate())
.peerPublic(aliceClassicA.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcA.getPrivate())
.buildResponder();
bobA.setPeerMessage(aliceA.getPeerMessage());
secretA = aliceA.deriveSecret();
byte[] responderA = bobA.deriveSecret();
System.out.println("...secretA=" + hex(secretA));
System.out.println("...responderA=" + hex(responderA));
assertArrayEquals(secretA, responderA);
aliceB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(aliceClassicB.getPrivate())
.peerPublic(bobClassicB.getPublic()).pqcKem().algorithm("ML-KEM").peerPublic(bobPqcB.getPublic())
.buildInitiator();
bobB = HybridKexBuilder.builder().profile(profile).transcript(transcriptB).classicAgreement()
.algorithm("Xdh").spec(XdhSpec.X25519).privateKey(bobClassicB.getPrivate())
.peerPublic(aliceClassicB.getPublic()).pqcKem().algorithm("ML-KEM").privateKey(bobPqcB.getPrivate())
.buildResponder();
bobB.setPeerMessage(aliceB.getPeerMessage());
secretB = aliceB.deriveSecret();
byte[] responderB = bobB.deriveSecret();
System.out.println("...secretB=" + hex(secretB));
System.out.println("...responderB=" + hex(responderB));
assertArrayEquals(secretB, responderB);
if (Arrays.equals(secretA, secretB)) {
throw new AssertionError("Transcript-bound secrets should differ for different transcripts");
}
} finally {
closeQuietly(aliceA);
closeQuietly(bobA);
closeQuietly(aliceB);
closeQuietly(bobB);
}
System.out.println("transcriptChangesDerivedSecret...ok");
}
private static void closeQuietly(HybridKexContext context) {
if (context == null) {
return;
}
try {
context.close();
} catch (Exception ignore) {
// Cleanup only.
}
}
private static String hex(byte[] value) {
if (value == null) {
return "null";
}
StringBuilder builder = new StringBuilder(value.length * 2);
for (int index = 0; index < value.length; index++) {
if (builder.length() >= 30) {
builder.append("...");
break;
}
int current = value[index] & 0xFF;
if (current < 16) {
builder.append('0');
}
builder.append(Integer.toHexString(current));
}
return builder.toString();
}
private static String lens(byte[] message) {
if (message == null || message.length < 8) {
return "classicLen=?, pqcLen=?";
}
try {
DataInputStream input = new DataInputStream(new ByteArrayInputStream(message));
int classicLength = input.readInt();
int pqcLength = 0;
if (message.length >= 8 + Math.max(0, classicLength)) {
if (classicLength > 0) {
input.skipBytes(classicLength);
}
pqcLength = input.readInt();
}
return "classicLen=" + classicLength + ", pqcLen=" + pqcLength;
} catch (Exception exception) {
return "classicLen=?, pqcLen=?";
}
}
}

View File

@@ -19,6 +19,13 @@
<attribute name="test" value="true"/> <attribute name="test" value="true"/>
</attributes> </attributes>
</classpathentry> </classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/> <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/> <classpathentry kind="output" path="bin/default"/>

View File

@@ -37,6 +37,8 @@ import java.nio.file.Path;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.api.PkiId; import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.audit.Principal; import zeroecho.pki.api.audit.Principal;
@@ -71,6 +73,7 @@ import zeroecho.pki.util.async.impl.DurableAsyncBus;
* </p> * </p>
*/ */
public final class FileBackedAsyncBusProvider implements AsyncBusProvider { public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
private static final Logger LOG = Logger.getLogger(FileBackedAsyncBusProvider.class.getName());
/** /**
* Configuration key for the log file path. * Configuration key for the log file path.
@@ -87,13 +90,29 @@ public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
return Set.of(KEY_LOG_PATH); return Set.of(KEY_LOG_PATH);
} }
/**
* Validates configuration for the durable file-backed async bus provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is invalid
*/
@Override
public void validateConfig(final ProviderConfig config) {
AsyncBusProvider.super.validateConfig(config);
Map<String, String> props = config.properties();
String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString());
Path.of(logPath);
for (String key : props.keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown async bus configuration key: " + key);
}
}
}
@Override @Override
public AsyncBus<PkiId, Principal, String, Object> allocate(ProviderConfig config) { public AsyncBus<PkiId, Principal, String, Object> allocate(ProviderConfig config) {
Objects.requireNonNull(config, "config"); Objects.requireNonNull(config, "config");
validateConfig(config);
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
Map<String, String> props = config.properties(); Map<String, String> props = config.properties();
String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString()); String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString());

View File

@@ -36,15 +36,26 @@ package zeroecho.pki.impl.audit;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink; import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider; import zeroecho.pki.spi.audit.AuditSinkProvider;
/** /**
* {@link AuditSinkProvider} for the file-backed audit sink.
* *
* <p>
* Supported configuration keys:
* </p>
* <ul>
* <li>{@code root} - audit storage root directory (required)</li>
* </ul>
*/ */
public class FileAuditSinkProvider implements AuditSinkProvider { public class FileAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(FileAuditSinkProvider.class.getName());
@Override @Override
public String id() { public String id() {
return "file"; return "file";
@@ -55,9 +66,29 @@ public class FileAuditSinkProvider implements AuditSinkProvider {
return Set.of("root"); return Set.of("root");
} }
/**
* Validates configuration for the file-backed audit sink provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@Override
public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
String rootString = config.require("root");
Path.of(rootString);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override @Override
public AuditSink allocate(ProviderConfig config) { public AuditSink allocate(ProviderConfig config) {
Objects.requireNonNull(config, "config"); Objects.requireNonNull(config, "config");
validateConfig(config);
String rootString = config.require("root"); String rootString = config.require("root");
Path root = Path.of(rootString); Path root = Path.of(rootString);

View File

@@ -35,15 +35,27 @@ package zeroecho.pki.impl.audit;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink; import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider; import zeroecho.pki.spi.audit.AuditSinkProvider;
/** /**
* {@link AuditSinkProvider} for the in-memory audit sink.
* *
* <p>
* Supported configuration keys:
* </p>
* <ul>
* <li>{@code size} - maximum retained event count (optional, positive
* integer)</li>
* </ul>
*/ */
public class InMemoryAuditSinkProvider implements AuditSinkProvider { public class InMemoryAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(InMemoryAuditSinkProvider.class.getName());
@Override @Override
public String id() { public String id() {
return "memory"; return "memory";
@@ -54,25 +66,54 @@ public class InMemoryAuditSinkProvider implements AuditSinkProvider {
return Set.of("size"); return Set.of("size");
} }
/**
* Validates configuration for the in-memory audit sink provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration contains an invalid
* {@code size} value
*/
@Override @Override
public AuditSink allocate(ProviderConfig config) { public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
Optional<String> sizeStr = config.get("size"); Optional<String> sizeStr = config.get("size");
if (sizeStr.isPresent()) { if (sizeStr.isPresent()) {
return new InMemoryAuditSink(parseIntOrDefault(sizeStr.get(), InMemoryAuditSink.DEFAULT_MAX_EVENTS)); int size = parseInt(sizeStr.get(), "size");
if (size <= 0) {
throw new IllegalArgumentException("Configuration key 'size' must be positive.");
}
}
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override
public AuditSink allocate(ProviderConfig config) {
validateConfig(config);
Optional<String> sizeStr = config.get("size");
if (sizeStr.isPresent()) {
return new InMemoryAuditSink(parseInt(sizeStr.get(), "size"));
} else { } else {
return new InMemoryAuditSink(); return new InMemoryAuditSink();
} }
} }
private static int parseIntOrDefault(String value, int defaultValue) { /**
if (value == null) { * Parses an integer configuration value.
return defaultValue; *
} * @param value raw configuration value
* @param keyName configuration key name
* @return parsed integer value
* @throws IllegalArgumentException if the value is not a valid integer
*/
private static int parseInt(final String value, final String keyName) {
try { try {
return Integer.parseInt(value); return Integer.parseInt(value);
} catch (NumberFormatException ex) { } catch (NumberFormatException ex) {
return defaultValue; throw new IllegalArgumentException("Configuration key '" + keyName + "' must be an integer.", ex);
} }
} }
} }

View File

@@ -35,15 +35,23 @@ package zeroecho.pki.impl.audit;
import java.util.Collections; import java.util.Collections;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSink; import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider; import zeroecho.pki.spi.audit.AuditSinkProvider;
/** /**
* {@link AuditSinkProvider} for the standard-output audit sink.
* *
* <p>
* This provider accepts no provider-specific configuration keys.
* </p>
*/ */
public class StdoutAuditSinkProvider implements AuditSinkProvider { public class StdoutAuditSinkProvider implements AuditSinkProvider {
private static final Logger LOG = Logger.getLogger(StdoutAuditSinkProvider.class.getName());
@Override @Override
public String id() { public String id() {
return "stdout"; return "stdout";
@@ -54,8 +62,24 @@ public class StdoutAuditSinkProvider implements AuditSinkProvider {
return Collections.emptySet(); return Collections.emptySet();
} }
/**
* Validates configuration for the standard-output audit sink provider.
*
* @param config provider configuration (never {@code null})
*/
@Override
public void validateConfig(final ProviderConfig config) {
AuditSinkProvider.super.validateConfig(config);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown audit sink configuration key: " + key);
}
}
}
@Override @Override
public AuditSink allocate(ProviderConfig config) { public AuditSink allocate(ProviderConfig config) {
validateConfig(config);
return new StdoutAuditSink(); return new StdoutAuditSink();
} }
} }

View File

@@ -35,6 +35,8 @@ package zeroecho.pki.impl.crypto.zeroecholib;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.crypto.SignatureWorkflow; import zeroecho.pki.spi.crypto.SignatureWorkflow;
@@ -61,6 +63,7 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
* </p> * </p>
*/ */
public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider { public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWorkflowProvider {
private static final Logger LOG = Logger.getLogger(ZeroEchoLibSignatureWorkflowProvider.class.getName());
private static final String KEY_KEYRING_PATH = "keyringPath"; private static final String KEY_KEYRING_PATH = "keyringPath";
private static final String KEY_KEYREF_PREFIX = "keyRefPrefix"; private static final String KEY_KEYREF_PREFIX = "keyRefPrefix";
@@ -76,17 +79,39 @@ public final class ZeroEchoLibSignatureWorkflowProvider implements SignatureWork
return Set.of(KEY_KEYRING_PATH, KEY_KEYREF_PREFIX, KEY_REQUIRE_SUFFIX); return Set.of(KEY_KEYRING_PATH, KEY_KEYREF_PREFIX, KEY_REQUIRE_SUFFIX);
} }
/**
* Validates configuration for the ZeroEcho-lib signature workflow provider.
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@Override @Override
public SignatureWorkflow allocate(ProviderConfig config) { public void validateConfig(final ProviderConfig config) {
if (config == null) { SignatureWorkflowProvider.super.validateConfig(config);
throw new IllegalArgumentException("config must not be null"); String keyringPath = config.require(KEY_KEYRING_PATH);
} Path.of(keyringPath);
config.get(KEY_KEYREF_PREFIX).ifPresent(value -> {
// Defensive hardening: fail fast if miswired config reaches this provider. if (value.isBlank()) {
if (!id().equals(config.backendId())) { throw new IllegalArgumentException("Configuration key '" + KEY_KEYREF_PREFIX + "' must not be blank.");
throw new IllegalArgumentException("ProviderConfig backendId mismatch."); }
});
config.get(KEY_REQUIRE_SUFFIX).ifPresent(value -> {
if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) {
throw new IllegalArgumentException(
"Configuration key '" + KEY_REQUIRE_SUFFIX + "' must be 'true' or 'false'.");
}
});
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown signature workflow configuration key: " + key);
}
} }
}
@Override
public SignatureWorkflow allocate(final ProviderConfig config) {
validateConfig(config);
String keyringPath = config.require(KEY_KEYRING_PATH); String keyringPath = config.require(KEY_KEYRING_PATH);
String prefix = config.get(KEY_KEYREF_PREFIX).orElse("zeroecho-lib:"); String prefix = config.get(KEY_KEYREF_PREFIX).orElse("zeroecho-lib:");
boolean requireSuffix = config.get(KEY_REQUIRE_SUFFIX).map(Boolean::parseBoolean).orElse(Boolean.TRUE); boolean requireSuffix = config.get(KEY_REQUIRE_SUFFIX).map(Boolean::parseBoolean).orElse(Boolean.TRUE);

View File

@@ -34,6 +34,8 @@
package zeroecho.pki.impl.framework.x509.bc; package zeroecho.pki.impl.framework.x509.bc;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.framework.CredentialFramework; import zeroecho.pki.spi.framework.CredentialFramework;
@@ -73,7 +75,9 @@ import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
* The current implementation does not consume any provider-specific * The current implementation does not consume any provider-specific
* configuration keys beyond backend selection through * configuration keys beyond backend selection through
* {@link ProviderConfig#backendId()}. Consequently, {@link #supportedKeys()} * {@link ProviderConfig#backendId()}. Consequently, {@link #supportedKeys()}
* returns an empty set. * returns an empty set and bootstrap will reject any
* {@code zeroecho.pki.framework.*} configuration keys other than the backend
* selector itself.
* </p> * </p>
* *
* <h2>Thread-safety</h2> * <h2>Thread-safety</h2>
@@ -83,6 +87,8 @@ import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
*/ */
public final class BcX509CredentialFrameworkProvider implements CredentialFrameworkProvider { public final class BcX509CredentialFrameworkProvider implements CredentialFrameworkProvider {
private static final Logger LOG = Logger.getLogger(BcX509CredentialFrameworkProvider.class.getName());
/** /**
* Returns the stable provider identifier used to select this X.509 framework * Returns the stable provider identifier used to select this X.509 framework
* backend. * backend.
@@ -110,6 +116,32 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
return Set.of(); return Set.of();
} }
/**
* Validates configuration for the Bouncy Castle backed X.509 credential
* framework provider.
*
* <p>
* This provider currently defines no provider-specific keys. Unknown keys are
* ignored for forward compatibility and are reported only by name through JUL
* warning output. The backend id must match {@link #id()}.
* </p>
*
* @param config provider configuration used to select this backend; must not be
* {@code null}
* @throws IllegalArgumentException if the backend id does not match this
* provider identifier
*/
@Override
public void validateConfig(final ProviderConfig config) {
CredentialFrameworkProvider.super.validateConfig(config);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown credential framework configuration key: " + key);
}
}
}
/** /**
* Allocates a new Bouncy Castle backed X.509 credential framework instance. * Allocates a new Bouncy Castle backed X.509 credential framework instance.
* *
@@ -129,12 +161,7 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
*/ */
@Override @Override
public CredentialFramework allocate(ProviderConfig config) { public CredentialFramework allocate(ProviderConfig config) {
if (config == null) { validateConfig(config);
throw new IllegalArgumentException("config must not be null");
}
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch");
}
return new BcX509CredentialFramework(); return new BcX509CredentialFramework();
} }
} }

View File

@@ -91,7 +91,9 @@
* issuance and status-object generation remain intentionally unsupported until * issuance and status-object generation remain intentionally unsupported until
* concrete backend components are supplied. This allows bootstrap and runtime * concrete backend components are supplied. This allows bootstrap and runtime
* composition to separate framework selection from full cryptographic and PKI * composition to separate framework selection from full cryptographic and PKI
* service wiring. * service wiring. The package is exposed to runtime composition only through
* the framework SPI and its ServiceLoader provider; bootstrap code must not
* depend on these implementation classes directly.
* </p> * </p>
* *
* <h2>Security considerations</h2> * <h2>Security considerations</h2>

View File

@@ -36,14 +36,15 @@ package zeroecho.pki.impl.fs;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
import zeroecho.pki.spi.audit.AuditSinkProvider;
import zeroecho.pki.spi.store.PkiStore; import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.PkiStoreProvider; import zeroecho.pki.spi.store.PkiStoreProvider;
/** /**
* {@link AuditSinkProvider} for the filesystem-backed {@link PkiStore}. * {@link PkiStoreProvider} for the filesystem-backed {@link PkiStore}.
* *
* <p> * <p>
* Supported configuration keys: * Supported configuration keys:
@@ -53,6 +54,7 @@ import zeroecho.pki.spi.store.PkiStoreProvider;
* </ul> * </ul>
*/ */
public final class FilesystemPkiStoreProvider implements PkiStoreProvider { public final class FilesystemPkiStoreProvider implements PkiStoreProvider {
private static final Logger LOG = Logger.getLogger(FilesystemPkiStoreProvider.class.getName());
/** /**
* Public no-arg constructor required by {@link java.util.ServiceLoader}. * Public no-arg constructor required by {@link java.util.ServiceLoader}.
@@ -71,9 +73,41 @@ public final class FilesystemPkiStoreProvider implements PkiStoreProvider {
return Set.of("root"); return Set.of("root");
} }
/**
* Validates configuration for the filesystem-backed PKI store provider.
*
* <p>
* Required configuration:
* </p>
* <ul>
* <li>{@code root}</li>
* </ul>
*
* <p>
* Unknown keys are ignored for forward compatibility and are reported by key
* name only.
* </p>
*
* @param config provider configuration (never {@code null})
* @throws IllegalArgumentException if the configuration is incomplete or
* invalid
*/
@Override @Override
public PkiStore allocate(ProviderConfig config) { public void validateConfig(final ProviderConfig config) {
PkiStoreProvider.super.validateConfig(config);
String rootString = config.require("root");
Path.of(rootString);
for (String key : config.properties().keySet()) {
if (!supportedKeys().contains(key) && LOG.isLoggable(Level.WARNING)) {
LOG.warning("Ignoring unknown PKI store configuration key: " + key);
}
}
}
@Override
public PkiStore allocate(final ProviderConfig config) {
Objects.requireNonNull(config, "config"); Objects.requireNonNull(config, "config");
validateConfig(config);
String rootString = config.require("root"); String rootString = config.require("root");
Path root = Path.of(rootString); Path root = Path.of(rootString);

View File

@@ -33,6 +33,7 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.spi; package zeroecho.pki.spi;
import java.util.Objects;
import java.util.Set; import java.util.Set;
/** /**
@@ -59,8 +60,10 @@ import java.util.Set;
* Configuration is provided as string properties through * Configuration is provided as string properties through
* {@link ProviderConfig}. Providers must validate required keys and reject * {@link ProviderConfig}. Providers must validate required keys and reject
* invalid configurations with {@link IllegalArgumentException}. Unknown keys * invalid configurations with {@link IllegalArgumentException}. Unknown keys
* must be ignored for forward compatibility. {@link #supportedKeys()} is * must be ignored for forward compatibility, but may be reported through
* informational and may be used for diagnostics or help output. * provider-local diagnostic logging that names keys only.
* {@link #supportedKeys()} is informational and may be used for diagnostics or
* help output.
* </p> * </p>
* *
* <h2>Security</h2> * <h2>Security</h2>
@@ -94,6 +97,37 @@ public interface ConfigurableProvider<T> {
*/ */
Set<String> supportedKeys(); Set<String> supportedKeys();
/**
* Validates the provided configuration before allocation.
*
* <p>
* Implementations should use this hook to enforce backend-specific invariants
* such as required keys and key formats. The default implementation already
* verifies that {@link ProviderConfig#backendId()} matches {@link #id()}.
* Unknown keys should normally be ignored for forward compatibility, but may be
* reported through provider-local logging that names keys only and never logs
* values.
* </p>
*
* <p>
* The default implementation performs a null check and verifies that the
* supplied backend id matches {@link #id()}. Providers with additional
* requirements should override this method, invoke the default implementation,
* and then validate their own keys.
* </p>
*
* @param config configuration to validate (never {@code null})
* @throws NullPointerException if {@code config} is {@code null}
* @throws IllegalArgumentException if the configuration is invalid
*/
default void validateConfig(final ProviderConfig config) {
Objects.requireNonNull(config, "config");
if (!id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
}
/** /**
* Allocates a new instance using the provided configuration. * Allocates a new instance using the provided configuration.
* *

View File

@@ -48,6 +48,8 @@ import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.audit.AuditSinkProvider; import zeroecho.pki.spi.audit.AuditSinkProvider;
import zeroecho.pki.spi.crypto.SignatureWorkflow; import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.crypto.SignatureWorkflowProvider; import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.framework.CredentialFrameworkProvider;
import zeroecho.pki.spi.store.PkiStore; import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.spi.store.PkiStoreProvider; import zeroecho.pki.spi.store.PkiStoreProvider;
import zeroecho.pki.util.async.AsyncBus; import zeroecho.pki.util.async.AsyncBus;
@@ -58,7 +60,7 @@ import zeroecho.pki.util.async.AsyncBus;
* <p> * <p>
* This class provides deterministic selection and instantiation rules for * This class provides deterministic selection and instantiation rules for
* components discovered via {@link java.util.ServiceLoader}. It is designed to * components discovered via {@link java.util.ServiceLoader}. It is designed to
* scale as more SPIs are introduced (audit, publish, framework integrations, * scale as more SPIs are introduced (audit, publish, credential frameworks,
* crypto/workflows, async orchestration, etc.). * crypto/workflows, async orchestration, etc.).
* </p> * </p>
* *
@@ -77,6 +79,10 @@ import zeroecho.pki.util.async.AsyncBus;
* <li>Select async bus provider: {@code -Dzeroecho.pki.async=&lt;id&gt;}</li> * <li>Select async bus provider: {@code -Dzeroecho.pki.async=&lt;id&gt;}</li>
* <li>Configure async bus provider: * <li>Configure async bus provider:
* {@code -Dzeroecho.pki.async.&lt;key&gt;=&lt;value&gt;}</li> * {@code -Dzeroecho.pki.async.&lt;key&gt;=&lt;value&gt;}</li>
* <li>Select credential framework provider:
* {@code -Dzeroecho.pki.framework=&lt;id&gt;}</li>
* <li>Configure credential framework provider:
* {@code -Dzeroecho.pki.framework.&lt;key&gt;=&lt;value&gt;}</li>
* </ul> * </ul>
* *
* <p> * <p>
@@ -100,6 +106,9 @@ public final class PkiBootstrap {
private static final String PROP_ASYNC_BACKEND = "zeroecho.pki.async"; private static final String PROP_ASYNC_BACKEND = "zeroecho.pki.async";
private static final String PROP_ASYNC_PREFIX = "zeroecho.pki.async."; private static final String PROP_ASYNC_PREFIX = "zeroecho.pki.async.";
private static final String PROP_FRAMEWORK_BACKEND = "zeroecho.pki.framework";
private static final String PROP_FRAMEWORK_PREFIX = "zeroecho.pki.framework.";
private PkiBootstrap() { private PkiBootstrap() {
throw new AssertionError("No instances."); throw new AssertionError("No instances.");
} }
@@ -264,6 +273,54 @@ public final class PkiBootstrap {
return provider.allocate(config); return provider.allocate(config);
} }
/**
* Opens a {@link CredentialFramework} using {@link CredentialFrameworkProvider}
* discovered via ServiceLoader.
*
* <p>
* Provider selection is deterministic and fail-fast:
* </p>
* <ul>
* <li>If {@code -Dzeroecho.pki.framework=&lt;id&gt;} is specified, the provider
* with the matching id is selected.</li>
* <li>If no id is specified and exactly one provider exists, that provider is
* selected.</li>
* <li>If multiple providers exist and no id is specified, bootstrap fails as
* configuration is ambiguous.</li>
* </ul>
*
* <p>
* Configuration properties are read from {@link System#getProperties()} using
* the prefix {@code zeroecho.pki.framework.}. Values are treated as sensitive
* and are never logged; only keys may be logged.
* </p>
*
* @return credential framework (never {@code null})
* @throws IllegalArgumentException if unsupported configuration keys are
* provided
* @throws IllegalStateException if provider selection fails
*/
public static CredentialFramework openCredentialFramework() {
String requestedId = System.getProperty(PROP_FRAMEWORK_BACKEND);
CredentialFrameworkProvider provider = SpiSelector.select(CredentialFrameworkProvider.class, requestedId,
new SpiSelector.ProviderId<>() {
@Override
public String id(CredentialFrameworkProvider p) {
return p.id();
}
});
Map<String, String> props = SpiSystemProperties.readPrefixed(PROP_FRAMEWORK_PREFIX);
ProviderConfig config = new ProviderConfig(provider.id(), props);
if (LOG.isLoggable(Level.INFO)) {
LOG.info("Selected credential framework provider: " + provider.id() + " (keys: " + props.keySet() + ")");
}
return provider.allocate(config);
}
/** /**
* Logs provider help information (supported keys) for diagnostics. * Logs provider help information (supported keys) for diagnostics.
* *

View File

@@ -36,8 +36,8 @@
* *
* <p> * <p>
* This package centralizes deterministic provider selection and a minimal * This package centralizes deterministic provider selection and a minimal
* configuration convention for multiple SPIs (store, audit, publication, and * configuration convention for multiple SPIs (store, audit, publication,
* future components). * credential frameworks, and future components).
* </p> * </p>
* *
* <p> * <p>

View File

@@ -33,8 +33,6 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.pki.spi.framework; package zeroecho.pki.spi.framework;
import java.util.Objects;
import zeroecho.pki.spi.ConfigurableProvider; import zeroecho.pki.spi.ConfigurableProvider;
import zeroecho.pki.spi.ProviderConfig; import zeroecho.pki.spi.ProviderConfig;
@@ -44,6 +42,7 @@ import zeroecho.pki.spi.ProviderConfig;
* <p> * <p>
* The PKI runtime selects a framework provider using * The PKI runtime selects a framework provider using
* {@link java.util.ServiceLoader} and instantiates it through * {@link java.util.ServiceLoader} and instantiates it through
* {@link #validateConfig(ProviderConfig)} and
* {@link #allocate(ProviderConfig)}. Provider selection is performed by * {@link #allocate(ProviderConfig)}. Provider selection is performed by
* {@code PkiBootstrap} using configuration properties (similarly to store, * {@code PkiBootstrap} using configuration properties (similarly to store,
* audit, async bus, and signature workflow providers). * audit, async bus, and signature workflow providers).
@@ -65,7 +64,9 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
* <p> * <p>
* Implementations must validate that {@link ProviderConfig#backendId()} matches * Implementations must validate that {@link ProviderConfig#backendId()} matches
* {@link #id()}. A mismatch must be reported as * {@link #id()}. A mismatch must be reported as
* {@link IllegalArgumentException}. * {@link IllegalArgumentException}. Implementations should invoke
* {@link #validateConfig(ProviderConfig)} from this method so that the same
* enforcement applies even when a provider is used outside bootstrap.
* </p> * </p>
* *
* @param config provider configuration (never {@code null}) * @param config provider configuration (never {@code null})
@@ -78,24 +79,4 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
@Override @Override
CredentialFramework allocate(ProviderConfig config); CredentialFramework allocate(ProviderConfig config);
/**
* Enforces that the provided configuration is intended for this provider.
*
* <p>
* This helper is intended for defensive checks inside provider implementations.
* </p>
*
* @param provider provider instance (never {@code null})
* @param config provider configuration (never {@code null})
* @throws NullPointerException if any argument is {@code null}
* @throws IllegalArgumentException if the backend id does not match
*/
static void requireIdMatch(final CredentialFrameworkProvider provider, final ProviderConfig config) {
Objects.requireNonNull(provider, "provider");
Objects.requireNonNull(config, "config");
if (!provider.id().equals(config.backendId())) {
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
}
}
} }

View File

@@ -40,5 +40,13 @@
* framework-specific fields. Framework-specific types must not leak into the * framework-specific fields. Framework-specific types must not leak into the
* public API. * public API.
* </p> * </p>
*
* <p>
* Runtime selection and allocation of concrete framework implementations is
* performed through {@link java.util.ServiceLoader} and the
* {@code CredentialFrameworkProvider} SPI. Bootstrap configuration follows the
* same deterministic, fail-fast conventions as the other PKI SPIs.
* </p>
*
*/ */
package zeroecho.pki.spi.framework; package zeroecho.pki.spi.framework;

View File

@@ -36,9 +36,9 @@
* *
* <p> * <p>
* SPIs are used to plug in persistence backends, audit sinks, publishing * SPIs are used to plug in persistence backends, audit sinks, publishing
* targets, and framework integrations while keeping the public PKI API * targets, credential frameworks, and other integrations while keeping the
* framework-agnostic. Implementations are typically discovered via * public PKI API framework-agnostic. Implementations are typically discovered
* {@link java.util.ServiceLoader}. * via {@link java.util.ServiceLoader}.
* </p> * </p>
* *
* <p> * <p>

View File

@@ -0,0 +1 @@
zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFrameworkProvider

View File

@@ -35,6 +35,7 @@ package zeroecho.pki.spi.bootstrap;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Properties; import java.util.Properties;
@@ -44,8 +45,13 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import zeroecho.pki.api.PkiId;
import zeroecho.pki.api.audit.Principal;
import zeroecho.pki.spi.audit.AuditSink; import zeroecho.pki.spi.audit.AuditSink;
import zeroecho.pki.spi.crypto.SignatureWorkflow;
import zeroecho.pki.spi.framework.CredentialFramework;
import zeroecho.pki.spi.store.PkiStore; import zeroecho.pki.spi.store.PkiStore;
import zeroecho.pki.util.async.AsyncBus;
/** /**
* JUnit 5 tests for {@link PkiBootstrap}. * JUnit 5 tests for {@link PkiBootstrap}.
@@ -75,9 +81,15 @@ public final class PkiBootstrapTest {
clearPrefix("zeroecho.pki.store."); clearPrefix("zeroecho.pki.store.");
clearPrefix("zeroecho.pki.audit."); clearPrefix("zeroecho.pki.audit.");
clearPrefix("zeroecho.pki.framework.");
clearPrefix("zeroecho.pki.async.");
clearPrefix("zeroecho.pki.crypto.workflow.");
System.clearProperty("zeroecho.pki.store"); System.clearProperty("zeroecho.pki.store");
System.clearProperty("zeroecho.pki.audit"); System.clearProperty("zeroecho.pki.audit");
System.clearProperty("zeroecho.pki.framework");
System.clearProperty("zeroecho.pki.async");
System.clearProperty("zeroecho.pki.crypto.workflow");
System.out.println("...tempDir=" + this.tempDir); System.out.println("...tempDir=" + this.tempDir);
System.out.println("...ok"); System.out.println("...ok");
@@ -162,6 +174,125 @@ public final class PkiBootstrapTest {
System.out.println("...ok"); System.out.println("...ok");
} }
@Test
public void openAudit_memory_rejectsInvalidSize() {
System.out.println("openAudit_memory_rejectsInvalidSize");
System.setProperty("zeroecho.pki.audit", "memory");
System.setProperty("zeroecho.pki.audit.size", "abc");
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> PkiBootstrap.openAudit());
System.out.println("...message=" + exception.getMessage());
assertEquals("Configuration key 'size' must be an integer.", exception.getMessage());
System.out.println("...ok");
}
@Test
public void openCredentialFramework_explicitX509BcProvider() {
System.out.println("openCredentialFramework_explicitX509BcProvider");
System.setProperty("zeroecho.pki.framework", "x509-bc");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openCredentialFramework_x509Bc_byDefault() {
System.out.println("openCredentialFramework_x509Bc_byDefault");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openCredentialFramework_x509Bc_ignoresUnknownKey() {
System.out.println("openCredentialFramework_x509Bc_ignoresUnknownKey");
System.setProperty("zeroecho.pki.framework", "x509-bc");
System.setProperty("zeroecho.pki.framework.unused", "value");
CredentialFramework framework = PkiBootstrap.openCredentialFramework();
assertNotNull(framework);
String frameworkClassName = framework.getClass().getName();
System.out.println("...frameworkClass=" + frameworkClassName);
assertEquals("zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFramework", frameworkClassName);
System.out.println("...ok");
}
@Test
public void openAsync_file_usesTempLogPath() {
System.out.println("openAsync_file_usesTempLogPath");
System.setProperty("zeroecho.pki.async", "file");
System.setProperty("zeroecho.pki.async.logPath", this.tempDir.resolve("async").resolve("async.log").toString());
AsyncBus<PkiId, Principal, String, Object> bus = PkiBootstrap.openAsyncBus();
assertNotNull(bus);
String busClassName = bus.getClass().getName();
System.out.println("...asyncClass=" + busClassName);
assertEquals("zeroecho.pki.util.async.impl.DurableAsyncBus", busClassName);
System.out.println("...ok");
}
@Test
public void openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath() {
System.out.println("openSignatureWorkflow_zeroEchoLib_usesConfiguredKeyringPath");
System.setProperty("zeroecho.pki.crypto.workflow", "zeroecho-lib");
System.setProperty("zeroecho.pki.crypto.workflow.keyringPath",
this.tempDir.resolve("workflow").resolve("keyring.zek").toString());
System.setProperty("zeroecho.pki.crypto.workflow.keyRefPrefix", "test-prefix:");
System.setProperty("zeroecho.pki.crypto.workflow.requireComponentSuffix", "false");
SignatureWorkflow workflow = PkiBootstrap.openSignatureWorkflow();
assertNotNull(workflow);
String workflowClassName = workflow.getClass().getName();
System.out.println("...workflowClass=" + workflowClassName);
assertEquals("zeroecho.pki.impl.crypto.zeroecholib.ZeroEchoLibSignatureWorkflow", workflowClassName);
System.out.println("...ok");
}
@Test
public void providerValidation_rejectsBackendIdMismatch() {
System.out.println("providerValidation_rejectsBackendIdMismatch");
zeroecho.pki.impl.fs.FilesystemPkiStoreProvider provider = new zeroecho.pki.impl.fs.FilesystemPkiStoreProvider();
zeroecho.pki.spi.ProviderConfig config = new zeroecho.pki.spi.ProviderConfig("other",
java.util.Map.of("root", this.tempDir.resolve("store").toString()));
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> provider.validateConfig(config));
System.out.println("...message=" + exception.getMessage());
assertEquals("ProviderConfig backendId mismatch.", exception.getMessage());
System.out.println("...ok");
}
private static void clearPrefix(String prefix) { private static void clearPrefix(String prefix) {
Properties props = System.getProperties(); Properties props = System.getProperties();
for (Object keyObj : props.keySet().toArray()) { for (Object keyObj : props.keySet().toArray()) {

View File

@@ -6,9 +6,9 @@ pluginManagement {
plugins { plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs // Apply the foojay-resolver plugin to allow automatic download of JDKs
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
} }
rootProject.name = 'ZeroEcho' rootProject.name = 'ZeroEcho'
include('app', 'lib', 'pki', 'samples') include('app', 'ext', 'lib', 'pki', 'samples')