Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion registration/configure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ rm ${work_dir}/registration-client/target/lib/provider.pem

cd "${work_dir}"

# ----------------------------------------------------------------------------------------------
# SECURITY : snapshot a TRUSTED classpath for the build-time signing tools NOW, while
# target/lib still contains ONLY the maven-built project jar + its resolved dependencies. The blocks
# below copy externally-downloaded / operator-supplied jars (third-party SDK, custom impls,
# registration-api-impl, icu4j, clamav) into target/lib. Running ManifestCreator / ManifestSigner off
# the full "lib/*" wildcard would let one of those untrusted jars shadow a tool class (or one of its
# dependency classes) and execute during signing with access to the keystore secret. ManifestCreator
# reads target/lib only as DATA (to hash it), so the tools never need those jars on their classpath.
trusted_tools_cp="$(ls "${work_dir}"/registration-client/target/lib/*.jar | tr '\n' ':')"
trusted_tools_cp="${trusted_tools_cp%:}"

if wget "${artifactory_url}/artifactory/libs-release-local/reg-client/resources.zip"
then
echo "Successfully downloaded reg-client resources, Adding it to reg-client jar"
Expand Down Expand Up @@ -118,7 +129,42 @@ cp "${work_dir}"/registration-client/target/run.bat "${work_dir}"/registration-c
jarsigner -keystore "${work_dir}"/build_files/keystore.p12 -storepass ${keystore_secret} -tsa ${signer_timestamp_url_env} -digestalg SHA-256 "${work_dir}"/registration-client/target/lib/registration-client-${client_version_env}.jar CodeSigning
jarsigner -keystore "${work_dir}"/build_files/keystore.p12 -storepass ${keystore_secret} -tsa ${signer_timestamp_url_env} -digestalg SHA-256 "${work_dir}"/registration-client/target/lib/registration-services-${client_version_env}.jar CodeSigning

java -cp "${work_dir}"/registration-client/target/registration-client-${client_version_env}.jar:"${work_dir}"/registration-client/target/lib/* io.mosip.registration.update.ManifestCreator "${client_version_env}" "${work_dir}/registration-client/target/lib" "${work_dir}/registration-client/target"
# ----------------------------------------------------------------------------------------------
# 1.3.0 dual manifest + detached SHA256withRSA signatures (issue #812).
# Verified at runtime by the launcher's SignatureVerifier using the public key in provider.pem
# (the cert of the same CodeSigning keypair in keystore.p12).
# ----------------------------------------------------------------------------------------------
keystore="${work_dir}/build_files/keystore.p12"
signing_alias="CodeSigning"
target_dir="${work_dir}/registration-client/target"
# Trusted, external-jar-free classpath snapshotted above (see SECURITY note before the downloads).
# Do NOT use "${target_dir}/lib/*" here: by this point lib/ also holds downloaded / operator-supplied
# jars that must never be on the signing-tool classpath.
java_cp="${trusted_tools_cp}"

# lib/MANIFEST.MF : per-file integrity hashes of everything under lib/ (bundled inside lib.zip)
# ManifestSigner reads the keystore password from the keystore_secret_env env var (not argv) so the
# secret never appears in process listings.
java -cp "${java_cp}" io.mosip.registration.update.ManifestCreator "${client_version_env}" "${target_dir}/lib" "${target_dir}/lib"
java -cp "${java_cp}" io.mosip.registration.update.ManifestSigner "${keystore}" "${signing_alias}" "${target_dir}/lib/MANIFEST.MF" "${target_dir}/lib/MANIFEST.MF.sig"

# jre21.zip : the JRE artifact referenced by the root manifest / downloaded by the launcher
cd "${target_dir}"
/usr/bin/zip -r jre21.zip jre

# Root ./MANIFEST.MF : orchestration artifacts only (NO lib.zip entry). Tolerant of sibling 1.3.0
# artifacts (_launcher.jar / migration.exe / rollback.exe) that may not exist yet -- those entries
# are skipped with a warning and fill in automatically as T2/T3/T4 land.
java -cp "${java_cp}" io.mosip.registration.update.ManifestCreator --list "${client_version_env}" "${target_dir}" \
"${target_dir}/jre21.zip" \
"${target_dir}/_launcher.jar" \
"${target_dir}/migration.exe" \
"${target_dir}/rollback.exe" \
"${target_dir}/run.bat"
java -cp "${java_cp}" io.mosip.registration.update.ManifestSigner "${keystore}" "${signing_alias}" "${target_dir}/MANIFEST.MF" "${target_dir}/MANIFEST.MF.sig"

# lib.zip : all of lib/** (including the signed lib/MANIFEST.MF) hosted from the upgrade server
/usr/bin/zip -r lib.zip lib

cd "${work_dir}"/registration-client/target/

Expand Down Expand Up @@ -154,6 +200,9 @@ mkdir -p /var/www/html/registration-test/${client_version_env}

cp "${work_dir}"/registration-client/target/lib/* /var/www/html/registration-client/${client_version_env}/lib
cp "${work_dir}"/registration-client/target/MANIFEST.MF /var/www/html/registration-client/${client_version_env}/
cp "${work_dir}"/registration-client/target/MANIFEST.MF.sig /var/www/html/registration-client/${client_version_env}/
cp "${work_dir}"/registration-client/target/lib.zip /var/www/html/registration-client/${client_version_env}/
cp "${work_dir}"/registration-client/target/jre21.zip /var/www/html/registration-client/${client_version_env}/
cp "${work_dir}"/build_files/maven-metadata.xml /var/www/html/registration-client/
cp "${work_dir}"/registration-client/target/reg-client.zip /var/www/html/registration-client/${client_version_env}/
cp "${work_dir}"/registration-test-utility.zip /var/www/html/registration-client/${client_version_env}/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,141 @@
package io.mosip.registration.update;

import io.mosip.kernel.core.util.FileUtils;
import io.mosip.kernel.core.util.HMACUtils2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

public class ManifestCreator {

private static final Logger logger = LoggerFactory.getLogger(ManifestCreator.class);
private static final String MANIFEST_FILE_NAME = "MANIFEST.MF";

private static final Manifest manifest = new Manifest();
private static final String LIST_MODE = "--list";

public static void main(String[] args) {
String version = args[0];
String libraryFolderPath = args[1];
String targetPath = args[2];

try {
File libFolder = new File(libraryFolderPath);
if(libFolder.exists() && libFolder.isDirectory()) {
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, version);
for(File file : libFolder.listFiles()) {
addEntryInManifest(file);
if (args.length == 0) {
throw new IllegalArgumentException(
"Usage: [--list <version> <targetPath> <file1> ...] OR <version> <libraryFolderPath> <targetPath>");
}
if (LIST_MODE.equals(args[0])) {
// --list <version> <targetPath> <file1> [<file2> ...]
// Builds the root MANIFEST.MF over an explicit, tolerant set of orchestration
// artifacts (jre21.zip, _launcher.jar, migration.exe, rollback.exe, run.bat).
// Files that do not exist yet (e.g. artifacts produced by other 1.3.0 sub-tasks)
// are skipped with a warning rather than failing the build.
if (args.length < 4) {
throw new IllegalArgumentException("Usage: --list <version> <targetPath> <file1> [<file2> ...]");
}
manifest.write(new FileOutputStream(targetPath + File.separator + MANIFEST_FILE_NAME));
System.out.println("Created " + MANIFEST_FILE_NAME);
String version = args[1];
String targetPath = args[2];
List<File> files = new ArrayList<>();
for (String path : Arrays.asList(args).subList(3, args.length)) {
files.add(new File(path));
}
createFromFiles(version, new File(targetPath, MANIFEST_FILE_NAME), files);
} else {
// <version> <libraryFolderPath> <targetPath>
if (args.length != 3) {
throw new IllegalArgumentException("Usage: <version> <libraryFolderPath> <targetPath>");
}
String version = args[0];
String libraryFolderPath = args[1];
String targetPath = args[2];
createFromFolder(version, new File(libraryFolderPath), new File(targetPath, MANIFEST_FILE_NAME));
}

} catch (Throwable e) {
} catch (Exception e) {
// Fail loudly with a non-zero exit so configure.sh (set -e) aborts the build instead of
// continuing to sign/package a stale or missing MANIFEST.MF. Errors (not Exceptions) are
// left to propagate uncaught — the JVM still exits non-zero, so the build still aborts.
logger.error("Failed to create the manifest", e);
System.exit(1);
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Builds a manifest containing one entry per file in {@code libFolder} (per-file integrity
* hashes). Used for {@code lib/MANIFEST.MF}, which is bundled inside {@code lib.zip}.
*/
static void createFromFolder(String version, File libFolder, File targetFile) throws Exception {
if (!(libFolder.exists() && libFolder.isDirectory())) {
// Throw (not return) so main() exits non-zero and configure.sh (set -e) aborts at the real
// cause, rather than continuing to sign a MANIFEST.MF that was never written.
throw new IllegalStateException("Library folder does not exist or is not a directory: " + libFolder);
}
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, version);
File[] children = libFolder.listFiles();
if (children == null) {
throw new IllegalStateException("Unable to list files in " + libFolder.getPath());
}
for (File file : children) {
if (file.isFile()) {
addEntryInManifest(manifest, file);
} else {
logger.warn("Skipping non-regular file in manifest folder: {}", file.getName());
}
}
write(manifest, targetFile);
}

private static void addEntryInManifest(File file) throws Exception {
String hashText = HMACUtils2.digestAsPlainText(FileUtils.readFileToByteArray(file));
/**
* Builds a manifest over an explicit list of files, tolerating files that are absent (logged and
* skipped). Used for the root {@code ./MANIFEST.MF}, whose orchestration artifacts may be
* produced by other 1.3.0 sub-tasks and not present at the time this runs.
*/
static void createFromFiles(String version, File targetFile, List<File> files) throws Exception {
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, version);
for (File file : files) {
if (file.exists() && file.isFile()) {
addEntryInManifest(manifest, file);
} else {
logger.warn("Skipping manifest entry, file not present yet: {}", file.getName());
}
}
write(manifest, targetFile);
}

private static void write(Manifest manifest, File targetFile) throws Exception {
try (FileOutputStream out = new FileOutputStream(targetFile)) {
manifest.write(out);
}
logger.info("Created {} ({} entries)", targetFile.getPath(), manifest.getEntries().size());
}

private static void addEntryInManifest(Manifest manifest, File file) throws Exception {
// Stream the file through SHA-256 rather than buffering it whole (jre21.zip is hundreds of MB).
// The result is byte-for-byte identical to HMACUtils2.digestAsPlainText(readFileToByteArray(file)):
// generateHash() is a SHA-256 MessageDigest.digest(), and digestAsPlainText formats it as
// encodeBytesToHex(digest, true /*upper-case*/, BIG_ENDIAN) — reused verbatim here so the
// manifest hashes stay compatible with the launcher's verifier and validateJarChecksum().
String hashText = HMACUtils2.encodeBytesToHex(sha256(file), true, ByteOrder.BIG_ENDIAN);
Attributes attribute = new Attributes();
attribute.put(Attributes.Name.CONTENT_TYPE, hashText);
manifest.getEntries().put(file.getName(), attribute);
}

/** Computes the SHA-256 digest of {@code file} by streaming it from disk (no full-file buffering). */
private static byte[] sha256(File file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream in = Files.newInputStream(file.toPath())) {
byte[] buffer = new byte[8192];
int read;
while ((read = in.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
}
return digest.digest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package io.mosip.registration.update;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.nio.file.Files;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.Signature;

/**
* Build-time signer that produces the detached {@code MANIFEST.MF.sig} files introduced in 1.3.0
* (issue #812). Signatures are {@code SHA256withRSA} over the raw manifest bytes, created with the
* existing code-signing key from {@code keystore.p12} &mdash; the same keypair whose certificate is
* shipped as {@code provider.pem} and used by the launcher's {@code SignatureVerifier} to verify.
* <p>
* Reads the private key straight out of the PKCS#12 keystore so the build never has to extract it to
* disk. Invoked from {@code configure.sh} via {@code java -cp ... io.mosip.registration.update.ManifestSigner}.
*/
public final class ManifestSigner {

private static final Logger logger = LoggerFactory.getLogger(ManifestSigner.class);
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final String KEYSTORE_TYPE = "PKCS12";
/**
* Environment variable carrying the keystore password. Read from the environment rather than a
* command-line argument so the secret is never exposed in process listings (ps, proc cmdline).
* Already present in the build container as a Dockerfile {@code ENV} and inherited by this process.
*/
private static final String STORE_PASS_ENV_VAR = "keystore_secret_env";

private ManifestSigner() {
// utility class
}

/**
* CLI entry point. The keystore password is read from the {@code keystore_secret_env} environment
* variable (NOT a command-line argument), so it is never exposed in process listings.
*
* @param args {@code <keystorePath> <alias> <dataFile> <sigFile>}
*/
public static void main(String[] args) {
if (args.length != 4) {
logger.error("Usage: ManifestSigner <keystorePath> <alias> <dataFile> <sigFile> "
+ "(keystore password is read from the {} environment variable)", STORE_PASS_ENV_VAR);
System.exit(2);
}
String storePass = System.getenv(STORE_PASS_ENV_VAR);
if (storePass == null || storePass.isEmpty()) {
logger.error("Keystore password not provided: set the {} environment variable", STORE_PASS_ENV_VAR);
System.exit(2);
}
try {
PrivateKey privateKey = loadPrivateKey(new File(args[0]), storePass.toCharArray(), args[1]);
signFile(new File(args[2]), new File(args[3]), privateKey);
logger.info("Signed {} -> {}", args[2], args[3]);
} catch (Exception e) {
logger.error("Failed to sign {}", args[2], e);
System.exit(1);
}
}

/**
* Loads the private key for {@code alias} from a PKCS#12 keystore, using the store password as the
* key password (the convention for the registration-client {@code keystore.p12}).
*/
public static PrivateKey loadPrivateKey(File keystoreFile, char[] storePass, String alias) throws Exception {
KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);
try (FileInputStream in = new FileInputStream(keystoreFile)) {
keyStore.load(in, storePass);
}
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, storePass);
if (privateKey == null) {
throw new IllegalArgumentException("No private key found for alias: " + alias);
}
return privateKey;
}

/** Writes a detached signature of {@code dataFile} to {@code sigFile}. */
public static void signFile(File dataFile, File sigFile, PrivateKey privateKey) throws Exception {
byte[] signature = sign(Files.readAllBytes(dataFile.toPath()), privateKey);
Files.write(sigFile.toPath(), signature);
}

/** Produces a detached {@code SHA256withRSA} signature over {@code data}. */
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature signer = Signature.getInstance(SIGNATURE_ALGORITHM);
signer.initSign(privateKey);
signer.update(data);
return signer.sign();
}
}
Loading
Loading