diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java b/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java
new file mode 100644
index 0000000..130f05b
--- /dev/null
+++ b/rpm/src/main/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessor.java
@@ -0,0 +1,221 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * mat1e, Groupe EDF - initial API and implementation
+ ********************************************************************************/
+package org.eclipse.packager.rpm.signature;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.compress.utils.IOUtils;
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPrivateKey;
+import org.bouncycastle.openpgp.PGPSecretKey;
+import org.bouncycastle.openpgp.PGPSecretKeyRing;
+import org.bouncycastle.openpgp.bc.BcPGPSecretKeyRing;
+import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder;
+import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider;
+import org.eclipse.packager.rpm.RpmSignatureTag;
+import org.eclipse.packager.rpm.Rpms;
+import org.eclipse.packager.rpm.header.Header;
+import org.eclipse.packager.rpm.header.Headers;
+import org.eclipse.packager.rpm.info.RpmInformation;
+import org.eclipse.packager.rpm.info.RpmInformations;
+import org.eclipse.packager.rpm.parse.RpmInputStream;
+
+/**
+ *
+ * Sign existing RPM file by calling
+ * {@link #perform(File, InputStream, String, OutputStream)}
+ *
+ *
+ */
+public class RpmFileSignatureProcessor {
+
+ private RpmFileSignatureProcessor() {
+ // Hide default constructor because of the static context
+ }
+
+ /**
+ *
+ * Perform the signature of the given RPM file with the given private key. This
+ * support only PGP. Write the result into the given {@link OutputStream}
+ *
+ *
+ * @param rpm : RPM file
+ * @param privateKeyIn : encrypted private key as {@link InputStream}
+ * @param passphrase : passphrase to decrypt the private key
+ * @param out : {@link OutputStream} to write to
+ * @throws IOException
+ * @throws PGPException
+ */
+ public static void perform(File rpm, InputStream privateKeyIn, String passphrase, OutputStream out)
+ throws IOException, PGPException {
+
+ final long leadLength = 96;
+ long signatureHeaderStart = 0L;
+ long signatureHeaderLength = 0L;
+ long payloadHeaderStart = 0L;
+ long payloadHeaderLength = 0L;
+ long payloadStart = 0L;
+ long archiveSize = 0L;
+ long payloadSize = 0L;
+ byte[] signatureHeader;
+
+ if (!rpm.exists()) {
+ throw new IOException("The file " + rpm.getName() + " does not exist");
+ }
+
+ // Extract private key
+ PGPPrivateKey privateKey = getPrivateKey(privateKeyIn, passphrase);
+
+ // Get the informations of the RPM
+ try (RpmInputStream rpmIn = new RpmInputStream(new FileInputStream(rpm))) {
+ signatureHeaderStart = rpmIn.getSignatureHeader().getStart();
+ signatureHeaderLength = rpmIn.getSignatureHeader().getLength();
+ payloadHeaderStart = rpmIn.getPayloadHeader().getStart();
+ payloadHeaderLength = rpmIn.getPayloadHeader().getLength();
+ RpmInformation info = RpmInformations.makeInformation(rpmIn);
+ payloadStart = info.getHeaderEnd();
+ archiveSize = info.getArchiveSize();
+ }
+
+ if (signatureHeaderStart == 0L || signatureHeaderLength == 0L || payloadHeaderStart == 0L
+ || payloadHeaderLength == 0L || payloadStart == 0L || archiveSize == 0L) {
+ throw new IOException("Unable to read " + rpm.getName() + " informations.");
+ }
+
+ // Build the signature header by digest payload header + payload
+ try (FileInputStream in = new FileInputStream(rpm)) {
+ FileChannel channelIn = in.getChannel();
+ payloadSize = channelIn.size() - payloadStart;
+ channelIn.position(leadLength + signatureHeaderLength);
+ ByteBuffer payloadHeaderBuff = ByteBuffer.allocate((int) payloadHeaderLength);
+ IOUtils.readFully(channelIn, payloadHeaderBuff);
+ ByteBuffer payloadBuff = ByteBuffer.allocate((int) payloadSize);
+ IOUtils.readFully(channelIn, payloadBuff);
+ signatureHeader = getSignature(privateKey, payloadHeaderBuff, payloadBuff, archiveSize);
+ }
+
+ // Write to the OutputStream
+ try (FileInputStream in = new FileInputStream(rpm)) {
+ IOUtils.copyRange(in, leadLength, out);
+ IOUtils.skip(in, signatureHeaderLength);
+ out.write(signatureHeader);
+ IOUtils.copy(in, out);
+ }
+ }
+
+ /**
+ *
+ * Sign the payload with its header with the given private key, see https://rpm-software-management.github.io/rpm/manual/format.html
+ *
+ *
+ * @param privateKey : private key already extracted
+ * @param payloadHeader : Payload's header as {@link ByteBuffer}
+ * @param payload : Payload as {@link ByteBuffer}
+ * @param archiveSize : archiveSize retrieved in {@link RpmInformation}
+ * @return the signature header as a bytes array
+ * @throws IOException
+ */
+ private static byte[] getSignature(PGPPrivateKey privateKey, ByteBuffer payloadHeader, ByteBuffer payload,
+ long archiveSize) throws IOException {
+ Header signatureHeader = new Header<>();
+ List signatureProcessors = getSignatureProcessors(privateKey);
+ payloadHeader.flip();
+ payload.flip();
+ for (SignatureProcessor processor : signatureProcessors) {
+ processor.init(archiveSize);
+ processor.feedHeader(payloadHeader.slice());
+ processor.feedPayloadData(payload.slice());
+ processor.finish(signatureHeader);
+ }
+ ByteBuffer signatureBuf = Headers.render(signatureHeader.makeEntries(), true, Rpms.IMMUTABLE_TAG_SIGNATURE);
+ final int payloadSize = signatureBuf.remaining();
+ final int padding = Rpms.padding(payloadSize);
+ byte[] signature = safeReadBuffer(signatureBuf);
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ result.write(signature);
+ if (padding > 0) {
+ result.write(safeReadBuffer(ByteBuffer.wrap(Rpms.EMPTY_128, 0, padding)));
+ }
+ return result.toByteArray();
+ }
+
+ /**
+ *
+ * Safe read (without buffer bytes) the given buffer and return it as a byte
+ * array
+ *
+ *
+ * @param buf : the {@link ByteBuffer} to read
+ * @return a bytes array
+ * @throws IOException
+ */
+ private static byte[] safeReadBuffer(ByteBuffer buf) throws IOException {
+ ByteArrayOutputStream result = new ByteArrayOutputStream();
+ while (buf.hasRemaining()) {
+ result.write(buf.get());
+ }
+ return result.toByteArray();
+ }
+
+ /**
+ *
+ * Return all {@link SignatureProcessor} required to perform signature
+ * {@link SignatureProcessors}
+ *
+ *
+ * @param privateKey : the private key, already extracted
+ *
+ * @return {@link List} of {@link SignatureProcessor}
+ */
+ private static List getSignatureProcessors(PGPPrivateKey privateKey) {
+ List signatureProcessors = new ArrayList<>();
+ signatureProcessors.add(SignatureProcessors.size());
+ signatureProcessors.add(SignatureProcessors.sha256Header());
+ signatureProcessors.add(SignatureProcessors.sha1Header());
+ signatureProcessors.add(SignatureProcessors.md5());
+ signatureProcessors.add(SignatureProcessors.payloadSize());
+ signatureProcessors.add(new RsaSignatureProcessor(privateKey));
+ return signatureProcessors;
+ }
+
+ /**
+ *
+ * Decrypt and retrieve the private key
+ *
+ *
+ * @param privateKeyIn : InputStream containing the encrypted private key
+ * @param passphrase : passphrase to decrypt private key
+ * @return private key as {@link PGPPrivateKey}
+ * @throws PGPException : if the private key cannot be extrated
+ * @throws IOException : if error happened with InputStream
+ */
+ private static PGPPrivateKey getPrivateKey(InputStream privateKeyIn, String passphrase)
+ throws PGPException, IOException {
+ ArmoredInputStream armor = new ArmoredInputStream(privateKeyIn);
+ PGPSecretKeyRing secretKeyRing = new BcPGPSecretKeyRing(armor);
+ PGPSecretKey secretKey = secretKeyRing.getSecretKey();
+ return secretKey.extractPrivateKey(new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider())
+ .build(passphrase.toCharArray()));
+ }
+}
diff --git a/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java b/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java
new file mode 100644
index 0000000..f894dc7
--- /dev/null
+++ b/rpm/src/test/java/org/eclipse/packager/rpm/signature/RpmFileSignatureProcessorTest.java
@@ -0,0 +1,134 @@
+/********************************************************************************
+ * Copyright (c) 2023 Contributors to the Eclipse Foundation
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * mat1e, Groupe EDF - initial API and implementation
+ ********************************************************************************/
+package org.eclipse.packager.rpm.signature;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.bouncycastle.bcpg.ArmoredInputStream;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.bc.BcPGPPublicKeyRing;
+import org.eclipse.packager.rpm.RpmSignatureTag;
+import org.eclipse.packager.rpm.Rpms;
+import org.eclipse.packager.rpm.parse.InputHeader;
+import org.eclipse.packager.rpm.parse.RpmInputStream;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+
+@TestMethodOrder(OrderAnnotation.class)
+public class RpmFileSignatureProcessorTest {
+
+ private static final String SOURCE_FILE_PATH = "src/test/resources/data/org.eclipse.scada-0.2.1-1.noarch.rpm";
+ private static final String PRIVATE_KEY_PATH = "src/test/resources/key/private_key.txt";
+ private static final String PUBLIC_KEY_PATH = "src/test/resources/key/public_key.txt";
+ private static final String RESULT_FILE_PATH = "src/test/resources/result/org.eclipse.scada-0.2.1-1.noarch.rpm";
+ private static final String RESULT_DIR = "src/test/resources/result";
+
+ @Test
+ @Order(1)
+ public void testSigningExistingRpm() throws IOException, PGPException {
+ // Read files
+ final String passPhrase = "testkey"; // Do not change
+ File rpm = new File(SOURCE_FILE_PATH);
+ File private_key = new File(PRIVATE_KEY_PATH);
+ if (!rpm.exists() || !private_key.exists()) {
+ fail("Input files rpm or private_key does not exist");
+ }
+ // Init the signed RPM
+ File resultDirectory = new File(RESULT_DIR);
+ resultDirectory.mkdir();
+ File signedRpm = new File(RESULT_FILE_PATH);
+ signedRpm.createNewFile();
+
+ try (FileOutputStream resultOut = new FileOutputStream(signedRpm);
+ InputStream privateKeyStream = new FileInputStream(private_key)) {
+ // Sign the RPM
+ RpmFileSignatureProcessor.perform(rpm, privateKeyStream, passPhrase, resultOut);
+
+ // Read the initial (non signed) rpm file
+ RpmInputStream initialRpm = new RpmInputStream(new FileInputStream(rpm));
+ initialRpm.available();
+ initialRpm.close();
+ InputHeader initialHeader = initialRpm.getSignatureHeader();
+
+ // Read the signed rpm file
+ RpmInputStream rpmSigned = new RpmInputStream(new FileInputStream(signedRpm));
+ rpmSigned.available();
+ rpmSigned.close();
+ InputHeader signedHeader = rpmSigned.getSignatureHeader();
+
+ // Get informations of the initial rpm file
+ int initialSize = (int) initialHeader.getEntry(RpmSignatureTag.SIZE).get().getValue();
+ int initialPayloadSize = (int) initialHeader.getEntry(RpmSignatureTag.PAYLOAD_SIZE).get().getValue();
+ String initialSha1 = initialHeader.getEntry(RpmSignatureTag.SHA1HEADER).get().getValue().toString();
+ String initialMd5 = Rpms.dumpValue(initialHeader.getEntry(RpmSignatureTag.MD5).get().getValue());
+
+ // Get informations of the signed rpm file
+ int signedSize = (int) signedHeader.getEntry(RpmSignatureTag.SIZE).get().getValue();
+ int signedPayloadSize = (int) signedHeader.getEntry(RpmSignatureTag.PAYLOAD_SIZE).get().getValue();
+ String signedSha1 = signedHeader.getEntry(RpmSignatureTag.SHA1HEADER).get().getValue().toString();
+ String signedMd5 = Rpms.dumpValue(signedHeader.getEntry(RpmSignatureTag.MD5).get().getValue());
+ String pgpSignature = Rpms.dumpValue(signedHeader.getEntry(RpmSignatureTag.PGP).get().getValue());
+
+ // Compare informations values of initial rpm and signed rpm
+ assertEquals(initialSize, signedSize);
+ assertEquals(initialPayloadSize, signedPayloadSize);
+ assertEquals(initialSha1, signedSha1);
+ assertEquals(initialMd5, signedMd5);
+
+ // Verify if signature is present
+ assertNotNull(pgpSignature);
+ }
+ }
+
+ @Test
+ @Order(2)
+ @Disabled
+ public void verifyRpmSignature() throws IOException, PGPException {
+ File public_key = new File(PUBLIC_KEY_PATH);
+ File signedRpm = new File(RESULT_FILE_PATH);
+ if (!public_key.exists() || !signedRpm.exists()) {
+ fail("Input files signedRpm or public_key does not exist");
+ }
+ InputStream publicKeyStream = new FileInputStream(public_key);
+ ArmoredInputStream armoredInputStream = new ArmoredInputStream(publicKeyStream);
+ PGPPublicKeyRing publicKeyRing = new BcPGPPublicKeyRing(armoredInputStream);
+ PGPPublicKey publicKey = publicKeyRing.getPublicKey();
+ // TODO Signature Check
+ }
+
+ @AfterAll
+ public static void clean() {
+ File resultDir = new File(RESULT_DIR);
+ File signedRpm = new File(RESULT_FILE_PATH);
+ if (resultDir.exists()) {
+ if (signedRpm.exists()) {
+ signedRpm.delete();
+ }
+ resultDir.delete();
+ }
+ }
+}
diff --git a/rpm/src/test/resources/key/private_key.txt b/rpm/src/test/resources/key/private_key.txt
new file mode 100644
index 0000000..a0e8804
--- /dev/null
+++ b/rpm/src/test/resources/key/private_key.txt
@@ -0,0 +1,116 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: OpenPGP.js v4.10.10
+Comment: https://openpgpjs.org
+
+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
+25uiY4mR
+=iZMt
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/rpm/src/test/resources/key/public_key.txt b/rpm/src/test/resources/key/public_key.txt
new file mode 100644
index 0000000..71cf1d4
--- /dev/null
+++ b/rpm/src/test/resources/key/public_key.txt
@@ -0,0 +1,56 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: OpenPGP.js v4.10.10
+Comment: https://openpgpjs.org
+
+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
+=wNfm
+-----END PGP PUBLIC KEY BLOCK-----