Skip to content
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

Feature to sign existing rpm file #46

Merged
merged 17 commits into from
Mar 31, 2023
Merged
Show file tree
Hide file tree
Changes from 12 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/********************************************************************************
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
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;

/**
mat1e marked this conversation as resolved.
Show resolved Hide resolved
*
* Sign existing RPM file by calling
* {@link #perform(InputStream, InputStream, String)}
*
*
*/
public class RpmFileSignatureProcessor {
mat1e marked this conversation as resolved.
Show resolved Hide resolved

private RpmFileSignatureProcessor() {
// Hide default constructor because of the static context
}

/**
* <p>
* Perform the signature of the given RPM file with the given private key. This
* support only PGP.
* </p>
*
* @param rpmIn : RPM file as an {@link InputStream}
* @param privateKeyIn : encrypted private key as {@link InputStream}
* @param passphrase : passphrase to decrypt the private key
* @return The signed RPM as an {@link OutputStream}
* @throws IOException
* @throws PGPException
*/
public static ByteArrayOutputStream perform(InputStream rpmIn, InputStream privateKeyIn, String passphrase)
throws IOException, PGPException {

PGPPrivateKey privateKey = getPrivateKey(privateKeyIn, passphrase);

ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(rpmIn, out);
byte[] buf = out.toByteArray();
RpmInputStream ref = getRpmInputStream(buf);
RpmInformation info = RpmInformations.makeInformation(ref);
ByteArrayInputStream data = new ByteArrayInputStream(buf);

byte[] lead = IOUtils.readRange(data, 96);
IOUtils.readRange(data, (int) ref.getSignatureHeader().getLength()); // skip existing signature header
byte[] payloadHeader = IOUtils.readRange(data, (int) ref.getPayloadHeader().getLength());
byte[] payload = IOUtils.toByteArray(data);

byte[] signature = buildSignature(privateKey, payloadHeader, payload, info.getArchiveSize());

ByteArrayOutputStream result = new ByteArrayOutputStream();
result.write(lead);
result.write(signature);
result.write(payloadHeader);
result.write(payload);

return result;
}

/**
* <p>
* Sign the payload with its header with the given private key and return the
* signature header as a bytes array. For more information about RPM format, see
* <a href=
* "https://rpm-software-management.github.io/rpm/manual/format.html">https://rpm-software-management.github.io/rpm/manual/format.html</a>
* </p>
*
* @param privateKey : private key already extracted
* @param payloadHeader : Payload's header as byte array
* @param payload : payload as byte array
* @param archiveSize : archiveSize retrieved in {@link RpmInformation}
* @return signature header as a bytes array
* @throws IOException
*/
private static byte[] buildSignature(PGPPrivateKey privateKey, byte[] payloadHeader, byte[] payload,
long archiveSize) throws IOException {
ByteBuffer headerBuf = bufBytes(payloadHeader);
ByteBuffer payloadBuf = bufBytes(payload);
Header<RpmSignatureTag> signatureHeader = new Header<>();
List<SignatureProcessor> signatureProcessors = getDefaultsSignatureProcessors();
signatureProcessors.add(new RsaSignatureProcessor(privateKey));
for (SignatureProcessor processor : signatureProcessors) {
headerBuf.clear();
payloadBuf.clear();
processor.init(archiveSize);
processor.feedHeader(headerBuf.slice());
processor.feedPayloadData(payloadBuf.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();
}

/**
* <p>
* Safe read (without buffer bytes) the given buffer and return it to a byte
* array
* </p>
*
* @param buf : the {@link ByteBuffer} to read
* @return byte array
*/
private static byte[] safeReadBuffer(ByteBuffer buf) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
while (buf.hasRemaining()) {
result.write(buf.get());
}
return result.toByteArray();
}

/**
* <p>
* Return all default {@link SignatureProcessor} defined in
* {@link SignatureProcessors}
* </p>
*
* @return {@link List<SignatureProcessor>} of {@link SignatureProcessor}
*/
private static List<SignatureProcessor> getDefaultsSignatureProcessors() {
List<SignatureProcessor> signatureProcessors = new ArrayList<>();
signatureProcessors.add(SignatureProcessors.size());
signatureProcessors.add(SignatureProcessors.sha256Header());
signatureProcessors.add(SignatureProcessors.sha1Header());
signatureProcessors.add(SignatureProcessors.md5());
signatureProcessors.add(SignatureProcessors.payloadSize());

return signatureProcessors;
}

/**
* <p>
* Convert an array of bytes into a ByteBuffer
* </p>
*
* @param data : byte array to convert
* @return a {@link ByteBuffer} built with data
* @throws IOException
*/
private static ByteBuffer bufBytes(byte[] data) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(data.length);
ReadableByteChannel headerChannel = Channels.newChannel(new ByteArrayInputStream(data));
IOUtils.readFully(headerChannel, buf);
return buf;
}

/**
* <p>
* Parse the byte[] to an RpmInputStream
* </p>
*
* @param buf : byte array representing the rpm file
* @return {@link RpmInputStream}
* @throws IOException
*/
private static RpmInputStream getRpmInputStream(byte[] buf) throws IOException {
try (RpmInputStream ref = new RpmInputStream(new ByteArrayInputStream(buf))) {
ref.available(); // init RpmInputStream
mat1e marked this conversation as resolved.
Show resolved Hide resolved
return ref;
}
}

/**
* <p>
* Decrypt and retrieve the private key
* </p>
*
* @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()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/********************************************************************************
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.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 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
String passPhrase = "testkey";
File rpm = new File("src/test/resources/data/org.eclipse.scada-0.2.1-1.noarch.rpm");
File private_key = new File("src/test/resources/key/private_key.txt");
if (!rpm.exists() || !private_key.exists()) {
fail("Input files rpm or private_key does not exist");
}
try (InputStream rpmStream = new FileInputStream(rpm)) {
try (InputStream privateKeyStream = new FileInputStream(private_key)) {
// Sign the RPM
try (ByteArrayOutputStream signedPackage = RpmFileSignatureProcessor.perform(rpmStream,
privateKeyStream, passPhrase)) {
byte[] bytes = signedPackage.toByteArray();

// Write 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)) {
resultOut.write(bytes);
resultOut.close();

// Read the initial (non signed) rpm file
RpmInputStream initialRpm = new RpmInputStream(new FileInputStream(rpm));
initialRpm.available();
initialRpm.close();
InputHeader<RpmSignatureTag> initialHeader = initialRpm.getSignatureHeader();
RpmInputStream rpmSigned = new RpmInputStream(new ByteArrayInputStream(bytes));
rpmSigned.available();
rpmSigned.close();
InputHeader<RpmSignatureTag> 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());

// Read information 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)
public void verifyRpmSignature() throws IOException, PGPException {
File public_key = new File("src/test/resources/key/public_key.txt");
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok if the test isn't there yet. But I think in this case, it should be removed.

Copy link
Contributor Author

@mat1e mat1e Mar 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is to keep the test file logic. Read rpm -> sign it -> write signed rpm -> read it -> verify signature -> remove it. If I remove this test, write the rpm after sign it became useless.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or complete the test before accepting the PR...

}

@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();
}
}
}
Loading