-
Notifications
You must be signed in to change notification settings - Fork 222
T5 (#812): build-pipeline dual manifest + detached signatures #816
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GOKULRAJ136
wants to merge
3
commits into
mosip:develop
Choose a base branch
from
GOKULRAJ136:T5-812-RC
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
130 changes: 112 additions & 18 deletions
130
...ion/registration-services/src/main/java/io/mosip/registration/update/ManifestCreator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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(); | ||
| } | ||
| } | ||
94 changes: 94 additions & 0 deletions
94
...tion/registration-services/src/main/java/io/mosip/registration/update/ManifestSigner.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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} — 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(); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.