Compare commits
8 Commits
impl-pki
...
release@1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
2333b01a3f
|
|||
|
de55ea909f
|
|||
|
a66c115a80
|
|||
|
e74e833c5b
|
|||
|
14fbf31989
|
|||
|
a4b9eeffe1
|
|||
|
e235d0e2b5
|
|||
|
d1bdf7d9df
|
@@ -9,8 +9,9 @@ dependencies {
|
||||
implementation 'org.apache.commons:commons-text'
|
||||
implementation 'commons-cli:commons-cli'
|
||||
implementation project(':lib')
|
||||
// might be removed if I move BC ops to the lib
|
||||
testImplementation 'org.bouncycastle:bcpkix-jdk18on'
|
||||
implementation project(':ext')
|
||||
// might be removed if I move BC ops to the lib
|
||||
testImplementation 'org.bouncycastle:bcpkix-jdk18on'
|
||||
}
|
||||
|
||||
application {
|
||||
|
||||
@@ -51,8 +51,8 @@ import org.apache.commons.cli.OptionGroup;
|
||||
import org.apache.commons.cli.Options;
|
||||
import org.apache.commons.cli.ParseException;
|
||||
|
||||
import zeroecho.sdk.integrations.covert.jpeg.JpegExifEmbedder;
|
||||
import zeroecho.sdk.integrations.covert.jpeg.Slot;
|
||||
import zeroecho.ext.integrations.covert.jpeg.JpegExifEmbedder;
|
||||
import zeroecho.ext.integrations.covert.jpeg.Slot;
|
||||
|
||||
/**
|
||||
* Command-line extension of ZeroEcho for covert embedding and extraction of
|
||||
|
||||
32
ext/.classpath
Normal file
32
ext/.classpath
Normal 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
23
ext/.project
Normal 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
31
ext/LICENSE
Normal 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
13
ext/build.gradle
Normal 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'
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.content.export;
|
||||
package zeroecho.ext.content.export;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.content.export;
|
||||
package zeroecho.ext.content.export;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -36,10 +36,10 @@
|
||||
*
|
||||
* <p>
|
||||
* 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
|
||||
* 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>
|
||||
*
|
||||
* <h2>Scope</h2>
|
||||
@@ -60,15 +60,15 @@
|
||||
* <li><i>Piwigo uploader</i> - an exportable content implementation
|
||||
* (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
|
||||
* built on {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and
|
||||
* built on {@link zeroecho.ext.content.api.AbstractExportableDataContent} and
|
||||
* honors
|
||||
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li>
|
||||
* {@link zeroecho.ext.content.api.ExportableDataContent.ExportMode}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Typical usage</h2>
|
||||
* <h3>Format a stream as Base64 command lines</h3> <pre>{@code
|
||||
* 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,
|
||||
* "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8),
|
||||
* 76,
|
||||
@@ -78,8 +78,8 @@
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Render an exportable content in a chosen mode</h3> <pre>{@code
|
||||
* zeroecho.sdk.content.api.ExportableDataContent content = ... some exportable content ...;
|
||||
* content.setExportMode(zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
|
||||
* zeroecho.ext.content.api.ExportableDataContent content = ... some exportable content ...;
|
||||
* content.setExportMode(zeroecho.ext.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
|
||||
* try (java.io.InputStream script = content.getStream()) {
|
||||
* script.transferTo(out);
|
||||
* }
|
||||
@@ -87,7 +87,7 @@
|
||||
*
|
||||
* <h2>Security notes</h2>
|
||||
* <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>
|
||||
* <li>Avoid embedding secrets in scripts; pass credentials via environment
|
||||
* variables or secure stores when possible.</li>
|
||||
@@ -98,8 +98,8 @@
|
||||
* <h2>Extensibility</h2>
|
||||
* <ul>
|
||||
* <li>New deployers should extend
|
||||
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and select a
|
||||
* default {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}
|
||||
* {@link zeroecho.ext.content.api.AbstractExportableDataContent} and select a
|
||||
* default {@link zeroecho.ext.content.api.ExportableDataContent.ExportMode}
|
||||
* appropriate for the platform.</li>
|
||||
* <li>Utilities like {@link Base64Stream} can be reused to generate
|
||||
* platform-friendly payloads without buffering whole files.</li>
|
||||
@@ -107,4 +107,4 @@
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.content.export;
|
||||
package zeroecho.ext.content.export;
|
||||
89
ext/src/main/java/zeroecho/ext/content/package-info.java
Normal file
89
ext/src/main/java/zeroecho/ext/content/package-info.java
Normal 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;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.covert;
|
||||
package zeroecho.ext.integrations.covert;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Map;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.covert.jpeg;
|
||||
package zeroecho.ext.integrations.covert.jpeg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.covert.jpeg;
|
||||
package zeroecho.ext.integrations.covert.jpeg;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -110,4 +110,4 @@
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.integrations.covert.jpeg;
|
||||
package zeroecho.ext.integrations.covert.jpeg;
|
||||
@@ -54,8 +54,8 @@
|
||||
*
|
||||
* <h2>Typical usage</h2> <pre>{@code
|
||||
* // Use the built-in English distribution to create a short cover text.
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator gen =
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator.EN;
|
||||
* zeroecho.ext.integrations.covert.TextualCodec.Generator gen =
|
||||
* zeroecho.ext.integrations.covert.TextualCodec.Generator.EN;
|
||||
* String sample = gen.getText(256);
|
||||
*
|
||||
* // 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('a', 8.2), java.util.Map.entry(' ', 25.4)
|
||||
* );
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator custom =
|
||||
* new zeroecho.sdk.integrations.covert.TextualCodec.Generator(freq);
|
||||
* zeroecho.ext.integrations.covert.TextualCodec.Generator custom =
|
||||
* new zeroecho.ext.integrations.covert.TextualCodec.Generator(freq);
|
||||
* String cover = custom.getText(128);
|
||||
* }</pre>
|
||||
*
|
||||
@@ -78,4 +78,4 @@
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.integrations.covert;
|
||||
package zeroecho.ext.integrations.covert;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
|
||||
/**
|
||||
* Enumeration of image formats supported for steganographic processing.
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
|
||||
/**
|
||||
* Metadata that describes a steganographic method.
|
||||
@@ -44,11 +44,11 @@
|
||||
* <h2>Design principles</h2>
|
||||
* <ul>
|
||||
* <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
|
||||
* and supplies a metadata descriptor.</li>
|
||||
* <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
|
||||
* provide support for lossy domains such as JPEG with dedicated
|
||||
* techniques.</li>
|
||||
@@ -64,7 +64,7 @@
|
||||
* the frequency domain (for example, DCT coefficients for JPEG) or more
|
||||
* advanced hybrid techniques. New methods can be registered by implementing the
|
||||
* {@code SteganographyMethod} interface and returning appropriate
|
||||
* {@link zeroecho.sdk.integrations.stegano.StegoMetadata}.
|
||||
* {@link zeroecho.ext.integrations.stegano.StegoMetadata}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Typical workflow</h2> <pre>{@code
|
||||
@@ -81,4 +81,4 @@
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.content.export;
|
||||
package zeroecho.ext.content.export;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.covert;
|
||||
package zeroecho.ext.integrations.covert;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.covert.jpeg;
|
||||
package zeroecho.ext.integrations.covert.jpeg;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
* (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.integrations.stegano;
|
||||
package zeroecho.ext.integrations.stegano;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 230 KiB |
@@ -13,13 +13,6 @@
|
||||
<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="src" output="bin/main" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="gradle_scope" value="main"/>
|
||||
|
||||
@@ -5,10 +5,19 @@ plugins {
|
||||
|
||||
group='org.egothor'
|
||||
|
||||
configurations {
|
||||
mockitoAgent
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'org.bouncycastle:bcpkix-jdk18on'
|
||||
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.overview = file("src/main/javadoc/overview.html")
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs("-javaagent:${configurations.mockitoAgent.singleFile}")
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ import javax.crypto.SecretKey;
|
||||
|
||||
import zeroecho.core.audit.AuditListener;
|
||||
import zeroecho.core.audit.AuditedContexts;
|
||||
import zeroecho.core.context.AgreementContext;
|
||||
import zeroecho.core.context.CryptoContext;
|
||||
import zeroecho.core.context.DigestContext;
|
||||
import zeroecho.core.context.EncryptionContext;
|
||||
@@ -335,33 +336,39 @@ public final class CryptoAlgorithms {
|
||||
|
||||
if (AUDIT_MODE == AuditMode.WRAP) {
|
||||
final AuditListener listener = AUDIT; // pass through the global listener
|
||||
if (ctx instanceof SignatureContext) {
|
||||
@SuppressWarnings("unchecked")
|
||||
C out = (C) AuditedContexts.wrap(ctx, listener, role);
|
||||
return out;
|
||||
} else if (ctx instanceof EncryptionContext) {
|
||||
@SuppressWarnings("unchecked")
|
||||
C out = (C) AuditedContexts.wrap(ctx, 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 switch (ctx) {
|
||||
case SignatureContext signatureContext -> wrapForAudit(signatureContext, listener, role);
|
||||
case EncryptionContext encryptionContext -> wrapForAudit(encryptionContext, listener, role);
|
||||
case KemContext kemContext -> wrapForAudit(kemContext, listener, role);
|
||||
case DigestContext digestContext -> wrapForAudit(digestContext, listener, role);
|
||||
case MacContext macContext -> wrapForAudit(macContext, listener, role);
|
||||
case AgreementContext agreementContext -> wrapForAudit(agreementContext, listener, role);
|
||||
};
|
||||
}
|
||||
|
||||
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 algorithm’s default spec for the
|
||||
* role.
|
||||
|
||||
@@ -232,6 +232,43 @@ public final class HybridKexBuilder {
|
||||
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.
|
||||
*
|
||||
@@ -241,22 +278,7 @@ public final class HybridKexBuilder {
|
||||
public HybridKexContext buildInitiator() throws IOException {
|
||||
validateCommon();
|
||||
|
||||
AgreementContext classic;
|
||||
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");
|
||||
}
|
||||
AgreementContext classic = buildClassicLeg();
|
||||
|
||||
if (pqcPeerPublic == null) {
|
||||
throw new IllegalStateException("pqc peer public must be set for initiator");
|
||||
@@ -279,22 +301,7 @@ public final class HybridKexBuilder {
|
||||
public HybridKexContext buildResponder() throws IOException {
|
||||
validateCommon();
|
||||
|
||||
AgreementContext classic;
|
||||
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");
|
||||
}
|
||||
AgreementContext classic = buildClassicLeg();
|
||||
|
||||
if (pqcPrivate == null) {
|
||||
throw new IllegalStateException("pqc private key must be set for responder");
|
||||
|
||||
@@ -54,10 +54,6 @@
|
||||
* {@link zeroecho.sdk.content.builtin.PlainString},
|
||||
* {@link zeroecho.sdk.content.builtin.PlainFile}, and
|
||||
* {@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>
|
||||
*
|
||||
* <h2>Responsibilities</h2>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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=?";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,13 @@
|
||||
<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"/>
|
||||
|
||||
@@ -37,6 +37,8 @@ import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.audit.Principal;
|
||||
@@ -71,6 +73,7 @@ import zeroecho.pki.util.async.impl.DurableAsyncBus;
|
||||
* </p>
|
||||
*/
|
||||
public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
|
||||
private static final Logger LOG = Logger.getLogger(FileBackedAsyncBusProvider.class.getName());
|
||||
|
||||
/**
|
||||
* Configuration key for the log file path.
|
||||
@@ -87,13 +90,29 @@ public final class FileBackedAsyncBusProvider implements AsyncBusProvider {
|
||||
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
|
||||
public AsyncBus<PkiId, Principal, String, Object> allocate(ProviderConfig config) {
|
||||
Objects.requireNonNull(config, "config");
|
||||
|
||||
if (!id().equals(config.backendId())) {
|
||||
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
|
||||
}
|
||||
validateConfig(config);
|
||||
|
||||
Map<String, String> props = config.properties();
|
||||
String logPath = props.getOrDefault(KEY_LOG_PATH, Path.of("pki-async").resolve("async.log").toString());
|
||||
|
||||
@@ -36,15 +36,26 @@ package zeroecho.pki.impl.audit;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
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 {
|
||||
private static final Logger LOG = Logger.getLogger(FileAuditSinkProvider.class.getName());
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return "file";
|
||||
@@ -55,9 +66,29 @@ public class FileAuditSinkProvider implements AuditSinkProvider {
|
||||
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
|
||||
public AuditSink allocate(ProviderConfig config) {
|
||||
Objects.requireNonNull(config, "config");
|
||||
validateConfig(config);
|
||||
|
||||
String rootString = config.require("root");
|
||||
Path root = Path.of(rootString);
|
||||
|
||||
@@ -35,15 +35,27 @@ package zeroecho.pki.impl.audit;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
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 {
|
||||
private static final Logger LOG = Logger.getLogger(InMemoryAuditSinkProvider.class.getName());
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return "memory";
|
||||
@@ -54,25 +66,54 @@ public class InMemoryAuditSinkProvider implements AuditSinkProvider {
|
||||
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
|
||||
public AuditSink allocate(ProviderConfig config) {
|
||||
|
||||
public void validateConfig(final ProviderConfig config) {
|
||||
AuditSinkProvider.super.validateConfig(config);
|
||||
Optional<String> sizeStr = config.get("size");
|
||||
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 {
|
||||
return new InMemoryAuditSink();
|
||||
}
|
||||
}
|
||||
|
||||
private static int parseIntOrDefault(String value, int defaultValue) {
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
/**
|
||||
* Parses an integer configuration value.
|
||||
*
|
||||
* @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 {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException ex) {
|
||||
return defaultValue;
|
||||
throw new IllegalArgumentException("Configuration key '" + keyName + "' must be an integer.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,15 +35,23 @@ package zeroecho.pki.impl.audit;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSink;
|
||||
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 {
|
||||
private static final Logger LOG = Logger.getLogger(StdoutAuditSinkProvider.class.getName());
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return "stdout";
|
||||
@@ -54,8 +62,24 @@ public class StdoutAuditSinkProvider implements AuditSinkProvider {
|
||||
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
|
||||
public AuditSink allocate(ProviderConfig config) {
|
||||
validateConfig(config);
|
||||
return new StdoutAuditSink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ package zeroecho.pki.impl.crypto.zeroecholib;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
@@ -61,6 +63,7 @@ import zeroecho.pki.spi.crypto.SignatureWorkflowProvider;
|
||||
* </p>
|
||||
*/
|
||||
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_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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public SignatureWorkflow allocate(ProviderConfig config) {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("config must not be null");
|
||||
}
|
||||
|
||||
// Defensive hardening: fail fast if miswired config reaches this provider.
|
||||
if (!id().equals(config.backendId())) {
|
||||
throw new IllegalArgumentException("ProviderConfig backendId mismatch.");
|
||||
public void validateConfig(final ProviderConfig config) {
|
||||
SignatureWorkflowProvider.super.validateConfig(config);
|
||||
String keyringPath = config.require(KEY_KEYRING_PATH);
|
||||
Path.of(keyringPath);
|
||||
config.get(KEY_KEYREF_PREFIX).ifPresent(value -> {
|
||||
if (value.isBlank()) {
|
||||
throw new IllegalArgumentException("Configuration key '" + KEY_KEYREF_PREFIX + "' must not be blank.");
|
||||
}
|
||||
});
|
||||
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 prefix = config.get(KEY_KEYREF_PREFIX).orElse("zeroecho-lib:");
|
||||
boolean requireSuffix = config.get(KEY_REQUIRE_SUFFIX).map(Boolean::parseBoolean).orElse(Boolean.TRUE);
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
package zeroecho.pki.impl.framework.x509.bc;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
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
|
||||
* configuration keys beyond backend selection through
|
||||
* {@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>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
@@ -83,6 +87,8 @@ import zeroecho.pki.spi.framework.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
|
||||
* backend.
|
||||
@@ -110,6 +116,32 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
|
||||
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.
|
||||
*
|
||||
@@ -129,12 +161,7 @@ public final class BcX509CredentialFrameworkProvider implements CredentialFramew
|
||||
*/
|
||||
@Override
|
||||
public CredentialFramework allocate(ProviderConfig config) {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("config must not be null");
|
||||
}
|
||||
if (!id().equals(config.backendId())) {
|
||||
throw new IllegalArgumentException("ProviderConfig backendId mismatch");
|
||||
}
|
||||
validateConfig(config);
|
||||
return new BcX509CredentialFramework();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,9 @@
|
||||
* issuance and status-object generation remain intentionally unsupported until
|
||||
* concrete backend components are supplied. This allows bootstrap and runtime
|
||||
* 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>
|
||||
*
|
||||
* <h2>Security considerations</h2>
|
||||
|
||||
@@ -36,14 +36,15 @@ package zeroecho.pki.impl.fs;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
import zeroecho.pki.spi.audit.AuditSinkProvider;
|
||||
import zeroecho.pki.spi.store.PkiStore;
|
||||
import zeroecho.pki.spi.store.PkiStoreProvider;
|
||||
|
||||
/**
|
||||
* {@link AuditSinkProvider} for the filesystem-backed {@link PkiStore}.
|
||||
* {@link PkiStoreProvider} for the filesystem-backed {@link PkiStore}.
|
||||
*
|
||||
* <p>
|
||||
* Supported configuration keys:
|
||||
@@ -53,6 +54,7 @@ import zeroecho.pki.spi.store.PkiStoreProvider;
|
||||
* </ul>
|
||||
*/
|
||||
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}.
|
||||
@@ -71,9 +73,41 @@ public final class FilesystemPkiStoreProvider implements PkiStoreProvider {
|
||||
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
|
||||
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");
|
||||
validateConfig(config);
|
||||
|
||||
String rootString = config.require("root");
|
||||
Path root = Path.of(rootString);
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.pki.spi;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -59,8 +60,10 @@ import java.util.Set;
|
||||
* Configuration is provided as string properties through
|
||||
* {@link ProviderConfig}. Providers must validate required keys and reject
|
||||
* invalid configurations with {@link IllegalArgumentException}. Unknown keys
|
||||
* must be ignored for forward compatibility. {@link #supportedKeys()} is
|
||||
* informational and may be used for diagnostics or help output.
|
||||
* must be ignored for forward compatibility, but may be reported through
|
||||
* provider-local diagnostic logging that names keys only.
|
||||
* {@link #supportedKeys()} is informational and may be used for diagnostics or
|
||||
* help output.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Security</h2>
|
||||
@@ -94,6 +97,37 @@ public interface ConfigurableProvider<T> {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -48,6 +48,8 @@ import zeroecho.pki.spi.audit.AuditSink;
|
||||
import zeroecho.pki.spi.audit.AuditSinkProvider;
|
||||
import zeroecho.pki.spi.crypto.SignatureWorkflow;
|
||||
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.PkiStoreProvider;
|
||||
import zeroecho.pki.util.async.AsyncBus;
|
||||
@@ -58,7 +60,7 @@ import zeroecho.pki.util.async.AsyncBus;
|
||||
* <p>
|
||||
* This class provides deterministic selection and instantiation rules for
|
||||
* 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.).
|
||||
* </p>
|
||||
*
|
||||
@@ -77,6 +79,10 @@ import zeroecho.pki.util.async.AsyncBus;
|
||||
* <li>Select async bus provider: {@code -Dzeroecho.pki.async=<id>}</li>
|
||||
* <li>Configure async bus provider:
|
||||
* {@code -Dzeroecho.pki.async.<key>=<value>}</li>
|
||||
* <li>Select credential framework provider:
|
||||
* {@code -Dzeroecho.pki.framework=<id>}</li>
|
||||
* <li>Configure credential framework provider:
|
||||
* {@code -Dzeroecho.pki.framework.<key>=<value>}</li>
|
||||
* </ul>
|
||||
*
|
||||
* <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_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() {
|
||||
throw new AssertionError("No instances.");
|
||||
}
|
||||
@@ -264,6 +273,54 @@ public final class PkiBootstrap {
|
||||
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=<id>} 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.
|
||||
*
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
*
|
||||
* <p>
|
||||
* This package centralizes deterministic provider selection and a minimal
|
||||
* configuration convention for multiple SPIs (store, audit, publication, and
|
||||
* future components).
|
||||
* configuration convention for multiple SPIs (store, audit, publication,
|
||||
* credential frameworks, and future components).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.pki.spi.framework;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import zeroecho.pki.spi.ConfigurableProvider;
|
||||
import zeroecho.pki.spi.ProviderConfig;
|
||||
|
||||
@@ -44,6 +42,7 @@ import zeroecho.pki.spi.ProviderConfig;
|
||||
* <p>
|
||||
* The PKI runtime selects a framework provider using
|
||||
* {@link java.util.ServiceLoader} and instantiates it through
|
||||
* {@link #validateConfig(ProviderConfig)} and
|
||||
* {@link #allocate(ProviderConfig)}. Provider selection is performed by
|
||||
* {@code PkiBootstrap} using configuration properties (similarly to store,
|
||||
* audit, async bus, and signature workflow providers).
|
||||
@@ -65,7 +64,9 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
|
||||
* <p>
|
||||
* Implementations must validate that {@link ProviderConfig#backendId()} matches
|
||||
* {@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>
|
||||
*
|
||||
* @param config provider configuration (never {@code null})
|
||||
@@ -78,24 +79,4 @@ public interface CredentialFrameworkProvider extends ConfigurableProvider<Creden
|
||||
@Override
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,5 +40,13 @@
|
||||
* framework-specific fields. Framework-specific types must not leak into the
|
||||
* public API.
|
||||
* </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;
|
||||
|
||||
@@ -36,9 +36,9 @@
|
||||
*
|
||||
* <p>
|
||||
* SPIs are used to plug in persistence backends, audit sinks, publishing
|
||||
* targets, and framework integrations while keeping the public PKI API
|
||||
* framework-agnostic. Implementations are typically discovered via
|
||||
* {@link java.util.ServiceLoader}.
|
||||
* targets, credential frameworks, and other integrations while keeping the
|
||||
* public PKI API framework-agnostic. Implementations are typically discovered
|
||||
* via {@link java.util.ServiceLoader}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
zeroecho.pki.impl.framework.x509.bc.BcX509CredentialFrameworkProvider
|
||||
@@ -35,6 +35,7 @@ package zeroecho.pki.spi.bootstrap;
|
||||
|
||||
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.nio.file.Path;
|
||||
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.io.TempDir;
|
||||
|
||||
import zeroecho.pki.api.PkiId;
|
||||
import zeroecho.pki.api.audit.Principal;
|
||||
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.util.async.AsyncBus;
|
||||
|
||||
/**
|
||||
* JUnit 5 tests for {@link PkiBootstrap}.
|
||||
@@ -75,9 +81,15 @@ public final class PkiBootstrapTest {
|
||||
|
||||
clearPrefix("zeroecho.pki.store.");
|
||||
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.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("...ok");
|
||||
@@ -162,6 +174,125 @@ public final class PkiBootstrapTest {
|
||||
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) {
|
||||
Properties props = System.getProperties();
|
||||
for (Object keyObj : props.keySet().toArray()) {
|
||||
|
||||
@@ -6,9 +6,9 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
// 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'
|
||||
|
||||
include('app', 'lib', 'pki', 'samples')
|
||||
include('app', 'ext', 'lib', 'pki', 'samples')
|
||||
|
||||
Reference in New Issue
Block a user