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 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
Original file line number Diff line number Diff line change
@@ -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;

/**
mat1e marked this conversation as resolved.
Show resolved Hide resolved
*
* Sign existing RPM file by calling
* {@link #perform(File, InputStream, String, OutputStream)}
*
*
*/
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. Write the result into the given {@link OutputStream}
* </p>
*
* @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);
}
}

/**
* <p>
* Sign the payload with its header with the given private key, 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 {@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<RpmSignatureTag> signatureHeader = new Header<>();
List<SignatureProcessor> 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();
}

/**
* <p>
* Safe read (without buffer bytes) the given buffer and return it as a byte
* array
* </p>
*
* @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();
}

/**
* <p>
* Return all {@link SignatureProcessor} required to perform signature
* {@link SignatureProcessors}
* </p>
*
* @param privateKey : the private key, already extracted
*
* @return {@link List<SignatureProcessor>} of {@link SignatureProcessor}
*/
private static List<SignatureProcessor> getSignatureProcessors(PGPPrivateKey privateKey) {
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());
signatureProcessors.add(new RsaSignatureProcessor(privateKey));
return signatureProcessors;
}

/**
* <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,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<RpmSignatureTag> initialHeader = initialRpm.getSignatureHeader();

// Read the signed rpm file
RpmInputStream rpmSigned = new RpmInputStream(new FileInputStream(signedRpm));
rpmSigned.available();
rpmSigned.close();
Comment on lines +78 to +80
Copy link
Contributor

Choose a reason for hiding this comment

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

@ctron I know this is an old PR, but I see this isn't really handled properly. I will make a PR.

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());

// 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
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