diff --git a/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreator.java b/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreator.java new file mode 100644 index 0000000..4eea770 --- /dev/null +++ b/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreator.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.security.pgp; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.openpgp.PGPPrivateKey; + +import java.io.OutputStream; +import java.util.function.Function; + +/** + * Implementation of {@link PgpSignerCreator} that depends on Bouncy Castle directly. + * Here, the user needs to pass in the {@link PGPPrivateKey} and digest algorithm they want to use + * for signing explicitly. + */ +public class BcPgpSignerCreator extends PgpSignerCreator { + + private final PGPPrivateKey privateKey; + private final int hashAlgorithm; + + /** + * Construct a {@link PgpSignerCreator} that uses Bouncy Castle classes directly and signs + * using a {@link SigningStream}. + * + * @param privateKey private signing key + * @param hashAlgorithmId OpenPGP hash algorithm ID of the digest algorithm to use for signing + * @param inlineSigned if true, use the cleartext signature framework to sign data inline. + * Otherwise, sign using detached signatures. + */ + public BcPgpSignerCreator(PGPPrivateKey privateKey, int hashAlgorithmId, boolean inlineSigned) { + super(inlineSigned); + if (hashAlgorithmId != 0) { + this.hashAlgorithm = hashAlgorithmId; + } else { + this.hashAlgorithm = HashAlgorithmTags.SHA256; + } + this.privateKey = privateKey; + } + + @Override + public Function createSigningStream() { + if (privateKey == null) { + return null; + } + + return outputStream -> new SigningStream(outputStream, privateKey, hashAlgorithm, inlineSigned); + } +} diff --git a/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreatorFactory.java b/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreatorFactory.java new file mode 100644 index 0000000..688fb68 --- /dev/null +++ b/core/src/main/java/org/eclipse/packager/security/pgp/BcPgpSignerCreatorFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.security.pgp; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; + +import java.util.NoSuchElementException; + +/** + * Implementation of the {@link PgpSignerCreatorFactory} that uses BC. + */ +public class BcPgpSignerCreatorFactory implements PgpSignerCreatorFactory { + + @Override + public PgpSignerCreator getSignerCreator( + PGPSecretKeyRing signingKey, + long signingKeyId, + char[] passphrase, + int hashAlgorithm, + boolean inlineSigned) { + PGPSecretKey key = signingKey.getSecretKey(signingKeyId); + if (key == null) { + throw new NoSuchElementException("No such signing key"); + } + try { + PGPPrivateKey privateKey = key.extractPrivateKey( + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build(passphrase)); + return new BcPgpSignerCreator(privateKey, hashAlgorithm, inlineSigned); + } catch (PGPException e) { + throw new RuntimeException("Could not unlock private key."); + } + } +} diff --git a/core/src/main/java/org/eclipse/packager/security/pgp/PgpHelper.java b/core/src/main/java/org/eclipse/packager/security/pgp/PgpHelper.java index b7169e0..8934753 100644 --- a/core/src/main/java/org/eclipse/packager/security/pgp/PgpHelper.java +++ b/core/src/main/java/org/eclipse/packager/security/pgp/PgpHelper.java @@ -133,4 +133,34 @@ public static PGPSecretKey loadSecretKey(final InputStream input, final String k return null; } + + public static PGPSecretKeyRing loadSecretKeyRing(final InputStream input, final String keyId) throws IOException, PGPException { + final long keyIdNum = Long.parseUnsignedLong(keyId, 16); + + final BcPGPSecretKeyRingCollection keyrings = new BcPGPSecretKeyRingCollection(PGPUtil.getDecoderStream(input)); + + final Iterator keyRingIter = keyrings.getKeyRings(); + while (keyRingIter.hasNext()) { + final PGPSecretKeyRing secretKeyRing = (PGPSecretKeyRing) keyRingIter.next(); + + final Iterator secretKeyIterator = secretKeyRing.getSecretKeys(); + while (secretKeyIterator.hasNext()) { + final PGPSecretKey key = (PGPSecretKey) secretKeyIterator.next(); + + if (!key.isSigningKey()) { + continue; + } + + final long shortId = key.getKeyID() & 0xFFFFFFFFL; + + if (key.getKeyID() != keyIdNum && shortId != keyIdNum) { + continue; + } + + return secretKeyRing; + } + } + + return null; + } } diff --git a/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreator.java b/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreator.java new file mode 100644 index 0000000..2a437f7 --- /dev/null +++ b/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.security.pgp; + +import java.io.OutputStream; +import java.util.function.Function; + +/** + * Factory for creating signing streams. + */ +public abstract class PgpSignerCreator { + + protected final boolean inlineSigned; + + public PgpSignerCreator(boolean inlineSigned) { + this.inlineSigned = inlineSigned; + } + + /** + * Return a {@link Function} that wraps an {@link OutputStream} into a signing stream. + * This method has no arguments (key, algorithms etc.) to be implementation agnostic. + * Subclasses shall pass those details as constructor arguments. + * + * @return transforming function + */ + public abstract Function createSigningStream(); +} diff --git a/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreatorFactory.java b/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreatorFactory.java new file mode 100644 index 0000000..11b1305 --- /dev/null +++ b/core/src/main/java/org/eclipse/packager/security/pgp/PgpSignerCreatorFactory.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.security.pgp; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; + +/** + * Factory interface for instantiating {@link PgpSignerCreator} classes. + * This class acts as the public interface for choosing the OpenPGP signing backend. + * By default, Bouncy Castle is used via {@link BcPgpSignerCreatorFactory}. + * TODO: Use dependency injection to allow optional dependencies to replace the default instance. + */ +public interface PgpSignerCreatorFactory { + + PgpSignerCreator getSignerCreator( + PGPSecretKeyRing signingKey, + long signingKeyId, + char[] passphrase, + int hashAlgorithm, + boolean inlineSigned); +} diff --git a/pgpainless/pom.xml b/pgpainless/pom.xml new file mode 100644 index 0000000..68289d1 --- /dev/null +++ b/pgpainless/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + + org.eclipse.packager + packager + 0.20.1-SNAPSHOT + + + packager-pgpainless + Eclipse Packager :: PGPainless Signer + + + + org.eclipse.packager + packager-core + ${project.version} + + + org.eclipse.packager + packager-rpm + ${project.version} + + + org.pgpainless + pgpainless-core + + + org.slf4j + slf4j-api + + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + + + ch.qos.logback + logback-classic + test + + + + + + + org.apache.felix + maven-bundle-plugin + + + + + diff --git a/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessHeaderSignatureProcessor.java b/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessHeaderSignatureProcessor.java new file mode 100644 index 0000000..5d5c8be --- /dev/null +++ b/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessHeaderSignatureProcessor.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature.pgpainless; + +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.eclipse.packager.rpm.RpmSignatureTag; +import org.eclipse.packager.rpm.header.Header; +import org.eclipse.packager.rpm.signature.SignatureProcessor; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * A {@link org.eclipse.packager.rpm.signature.SignatureProcessor} for signing headers, which uses PGPainless as + * backend for the signing operation. + * This processor can be used with RSA or EdDSA signing (sub-)keys and will produce RPMv4 header signatures + * which are emitted with either the {@link RpmSignatureTag#RSAHEADER} or {@link RpmSignatureTag#DSAHEADER} + * header tag. + */ +public class PGPainlessHeaderSignatureProcessor implements SignatureProcessor { + + private final Logger logger = LoggerFactory.getLogger(PGPainlessHeaderSignatureProcessor.class); + + private final EncryptionStream signingStream; + + public PGPainlessHeaderSignatureProcessor(PGPSecretKeyRing key, SecretKeyRingProtector keyProtector) { + this(key, keyProtector, 0); + } + + public PGPainlessHeaderSignatureProcessor(PGPSecretKeyRing key, SecretKeyRingProtector keyProtector, int hashAlgorithm) { + OutputStream sink = new OutputStream() { + @Override + public void write(int i) { + // Discard plaintext + } + }; + SigningOptions signingOptions = SigningOptions.get(); + if (hashAlgorithm != 0) { + signingOptions.overrideHashAlgorithm(HashAlgorithm.requireFromId(hashAlgorithm)); + } + try { + signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(sink) + .withOptions( + ProducerOptions.sign( + signingOptions.addDetachedSignature(keyProtector, key) + ).setAsciiArmor(false) + .overrideCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED) + ); + } catch (PGPException | IOException e) { + throw new RuntimeException(e); + } + } + + public Logger getLogger() { + return logger; + } + + @Override + public void feedHeader(ByteBuffer header) { + feedData(header); + } + + @Override + public void feedPayloadData(ByteBuffer data) { + // We only work on header data + } + + private void feedData(ByteBuffer data) { + try { + if (data.hasArray()) { + signingStream.write(data.array(), data.position(), data.remaining()); + } else { + final byte[] buffer = new byte[data.remaining()]; + data.get(buffer); + signingStream.write(buffer); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void finish(Header signature) { + try { + signingStream.close(); + EncryptionResult result = signingStream.getResult(); + PGPSignature pgpSignature = result.getDetachedSignatures().flatten().iterator().next(); + byte[] value = pgpSignature.getEncoded(); + switch (pgpSignature.getKeyAlgorithm()) { + // RSA + case PublicKeyAlgorithmTags.RSA_GENERAL: // 1 + getLogger().info("RSA HEADER: {}", value); + signature.putBlob(RpmSignatureTag.RSAHEADER, value); + break; + + // DSA + // https://rpm-software-management.github.io/rpm/manual/format_v4.html is talking about "EcDSA", + // which is probably a typo. + case PublicKeyAlgorithmTags.DSA: // 17 + case PublicKeyAlgorithmTags.EDDSA_LEGACY: // 22 + getLogger().info("DSA HEADER: {}", value); + signature.putBlob(RpmSignatureTag.DSAHEADER, value); + break; + + default: + throw new RuntimeException("Unsupported public key algorithm id: " + pgpSignature.getKeyAlgorithm()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessSignatureProcessorFactory.java b/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessSignatureProcessorFactory.java new file mode 100644 index 0000000..35f568d --- /dev/null +++ b/pgpainless/src/main/java/org/eclipse/packager/rpm/signature/pgpainless/PGPainlessSignatureProcessorFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature.pgpainless; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.eclipse.packager.rpm.signature.PgpSignatureProcessorFactory; +import org.eclipse.packager.rpm.signature.SignatureProcessor; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; + +public class PGPainlessSignatureProcessorFactory extends PgpSignatureProcessorFactory { + + private final PGPSecretKeyRing key; + private final SecretKeyRingProtector keyProtector; + private final int hashAlgorithm; + + public PGPainlessSignatureProcessorFactory(PGPSecretKeyRing key, char[] passphrase) { + this(key, passphrase, 0); // Leave hash algorithm up to the signing backend to choose. + } + + public PGPainlessSignatureProcessorFactory(PGPSecretKeyRing key, char[] passphrase, int hashAlgorithm) { + this.key = key; + this.keyProtector = SecretKeyRingProtector.unlockAnyKeyWith(new Passphrase(passphrase)); + this.hashAlgorithm = hashAlgorithm; + } + + @Override + public SignatureProcessor createHeaderSignatureProcessor() { + if (hashAlgorithm != 0) { + return new PGPainlessHeaderSignatureProcessor(key, keyProtector, hashAlgorithm); + } else { + return new PGPainlessHeaderSignatureProcessor(key, keyProtector); + } + } +} diff --git a/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreator.java b/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreator.java new file mode 100644 index 0000000..77869fd --- /dev/null +++ b/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreator.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.security.pgp.pgpainless; + +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.eclipse.packager.security.pgp.BcPgpSignerCreator; +import org.eclipse.packager.security.pgp.PgpSignerCreator; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.DocumentSignatureType; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.encryption_signing.EncryptionResult; +import org.pgpainless.encryption_signing.EncryptionStream; +import org.pgpainless.encryption_signing.ProducerOptions; +import org.pgpainless.encryption_signing.SigningOptions; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.ArmoredOutputStreamFactory; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Set; +import java.util.function.Function; + +/** + * {@link PgpSignerCreator} that uses PGPainless as a wrapper around Bouncy Castle. + * Contrary to {@link BcPgpSignerCreator}, this class validates that the + * signing key is healthy and ensures that safe algorithms are used. + */ +public class PGPainlessSignerCreator extends PgpSignerCreator { + + private final SigningOptions signing = SigningOptions.get(); + + /** + * Create a new {@link PGPainlessSignerCreator}. + * If inlineSigned is true, the output will be an inline-signed message using the + * Cleartext Signature Framework (CSF). + * Else it will be ASCII armored detached signatures. + * + * @param inlineSigned whether we want to CSF inline sign or detached sign + */ + public PGPainlessSignerCreator(PGPSecretKeyRing key, SecretKeyRingProtector keyProtector, int hashAlgorithm, boolean inlineSigned) { + super(inlineSigned); + if (hashAlgorithm != 0) { + signing.overrideHashAlgorithm(HashAlgorithm.requireFromId(hashAlgorithm)); + } + try { + if (inlineSigned) { + signing.addInlineSignature(keyProtector, key, DocumentSignatureType.BINARY_DOCUMENT); + } else { + signing.addDetachedSignature(keyProtector, key, DocumentSignatureType.BINARY_DOCUMENT); + } + } catch (PGPException e) { + throw new RuntimeException(e); + } + } + + @Override + public Function createSigningStream() { + return outputStream -> { + ProducerOptions options = ProducerOptions.sign(signing) + .setAsciiArmor(true); + + if (inlineSigned) { + options.setCleartextSigned(); + } + + try { + return new PGPainlessSigningStream(options, outputStream); + } catch (IOException | PGPException e) { + throw new RuntimeException(e); + } + }; + } + + /** + * Depending on whether we inline-sign, we either write the plaintext signed using the + * Cleartext Signature Framework, or just the ASCII armored detached signature(s). + */ + public static class PGPainlessSigningStream extends OutputStream { + + private final ProducerOptions options; + private final EncryptionStream signingStream; + private final OutputStream outputStream; // if we detached-sign, we emit signatures here + + public PGPainlessSigningStream(ProducerOptions options, OutputStream outputStream) + throws PGPException, IOException { + this.options = options; + this.outputStream = outputStream; + + if (options.isCleartextSigned()) { + // emit CSF-wrapped plaintext with inline signatures + this.signingStream = PGPainless.encryptAndOrSign() + .onOutputStream(outputStream) + .withOptions(options); + } else { + // Emit just the detached, armored signatures to output stream + OutputStream plaintextSink = new OutputStream() { + @Override + public void write(int i) { + // Discard plaintext bytes + } + }; + this.signingStream = PGPainless.encryptAndOrSign() + // TODO: Replace with .discardOutput() with PGPainless 1.6.8+ + .onOutputStream(plaintextSink) + .withOptions(options); + } + } + + @Override + public void write(int i) throws IOException { + signingStream.write(i); + } + + @Override + public void write(byte[] b) throws IOException { + signingStream.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + signingStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + signingStream.flush(); + } + + @Override + public void close() throws IOException { + signingStream.close(); + + // If we detached-sign, emit signatures + if (!options.isCleartextSigned()) { + ArmoredOutputStream armorOut = ArmoredOutputStreamFactory.get(outputStream); + + EncryptionResult result = signingStream.getResult(); + result.getDetachedSignatures().flatten().forEach(sig -> { + try { + sig.encode(armorOut); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + armorOut.close(); + } + } + + public Set getDetachedSignatures() { + return signingStream.getResult().getDetachedSignatures().flatten(); + } + } +} diff --git a/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreatorFactory.java b/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreatorFactory.java new file mode 100644 index 0000000..03e7836 --- /dev/null +++ b/pgpainless/src/main/java/org/eclipse/packager/security/pgp/pgpainless/PGPainlessSignerCreatorFactory.java @@ -0,0 +1,24 @@ +package org.eclipse.packager.security.pgp.pgpainless; + +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.eclipse.packager.security.pgp.PgpSignerCreator; +import org.eclipse.packager.security.pgp.PgpSignerCreatorFactory; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; + +public class PGPainlessSignerCreatorFactory implements PgpSignerCreatorFactory { + + @Override + public PgpSignerCreator getSignerCreator( + PGPSecretKeyRing signingKey, + long signingKeyId, + char[] passphrase, + int hashAlgorithm, + boolean inlineSigned) { + return new PGPainlessSignerCreator( + signingKey, + SecretKeyRingProtector.unlockAnyKeyWith(new Passphrase(passphrase)), + hashAlgorithm, + inlineSigned); + } +} diff --git a/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/HeaderSignatureProcessorTest.java b/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/HeaderSignatureProcessorTest.java new file mode 100644 index 0000000..0f495e1 --- /dev/null +++ b/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/HeaderSignatureProcessorTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature.pgpainless; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.util.io.Streams; +import org.eclipse.packager.rpm.RpmSignatureTag; +import org.eclipse.packager.rpm.header.Header; +import org.eclipse.packager.rpm.signature.BcPgpSignatureProcessorFactory; +import org.eclipse.packager.rpm.signature.PgpSignatureProcessorFactory; +import org.eclipse.packager.rpm.signature.SignatureProcessor; +import org.junit.jupiter.api.Named; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.pgpainless.PGPainless; +import org.pgpainless.decryption_verification.ConsumerOptions; +import org.pgpainless.decryption_verification.DecryptionStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class HeaderSignatureProcessorTest { + + public static Stream pgpSignatureProcessorFactoriesForKey(TestKey key, int hashAlgorithm) throws PGPException { + PGPPrivateKey signingKey = key.getKey().getSecretKey(key.getSigningKeyId()).extractPrivateKey( + new BcPBESecretKeyDecryptorBuilder(new BcPGPDigestCalculatorProvider()) + .build(key.getPassphrase()) + ); + return Stream.of( + Arguments.of( + Named.of("Bouncy Castle", new BcPgpSignatureProcessorFactory(signingKey, hashAlgorithm))), + Arguments.of( + Named.of("PGPainless", new PGPainlessSignatureProcessorFactory(key.getKey(), key.getPassphrase(), hashAlgorithm))) + ); + } + + public void verifySignature(boolean expectSuccess, TestKey key, byte[] data, Header header, RpmSignatureTag tag) throws PGPException, IOException { + if (expectSuccess) { + ByteBuffer sig = (ByteBuffer) header.get(tag); + assertNotNull(sig, "Implementation did not populate " + tag + " header"); + + DecryptionStream verificationStream = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(data)) + .withOptions(ConsumerOptions.get() + .addVerificationCert(PGPainless.extractCertificate(key.getKey())) + .addVerificationOfDetachedSignatures(new ByteArrayInputStream(sig.array())) + ); + + Streams.drain(verificationStream); // Process all plaintext + verificationStream.close(); // finalize verification + + assertTrue(verificationStream.getMetadata().isVerifiedSigned()); + } else { + assertNull(header.get(tag)); + } + } + + + public static Stream factoriesForComplexRsaKeyWithPrimaryKeyId() throws PGPException { + return pgpSignatureProcessorFactoriesForKey(TestKey.COMPLEX_RSA_KEY_MISMATCHED_KEYID, HashAlgorithmTags.SHA256); + } + + @ParameterizedTest + @MethodSource("factoriesForComplexRsaKeyWithPrimaryKeyId") + public void testWithComplexRsaKeyWithProvidedPrimaryKeyId(PgpSignatureProcessorFactory factory) throws PGPException, IOException { + byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + SignatureProcessor processor = factory.createHeaderSignatureProcessor(); + processor.feedHeader(ByteBuffer.wrap(data)); + Header header = new Header<>(); + processor.finish(header); + + verifySignature(true, TestKey.COMPLEX_RSA_KEY_MISMATCHED_KEYID, data, header, RpmSignatureTag.RSAHEADER); + } + + + public static Stream factoriesForECDSAKey() throws PGPException { + return pgpSignatureProcessorFactoriesForKey(TestKey.FRESH_ECDSA_KEY, HashAlgorithmTags.SHA256); + } + + @ParameterizedTest + @MethodSource("factoriesForECDSAKey") + public void testWithECDSAKey(PgpSignatureProcessorFactory factory) throws PGPException, IOException { + byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + SignatureProcessor processor = factory.createHeaderSignatureProcessor(); + processor.feedHeader(ByteBuffer.wrap(data)); + Header header = new Header<>(); + try { + processor.finish(header); + } catch (RuntimeException e) { + // expected + return; + } + verifySignature(false, TestKey.FRESH_ECDSA_KEY, data, header, RpmSignatureTag.DSAHEADER); + } + + + public static Stream factoriesForEdDSAKey() throws PGPException { + return pgpSignatureProcessorFactoriesForKey(TestKey.FRESH_EDDSA_KEY, HashAlgorithmTags.SHA256); + } + + @ParameterizedTest + @MethodSource("factoriesForEdDSAKey") + public void testWithEdDSAKey(PgpSignatureProcessorFactory factory) throws PGPException, IOException { + byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + SignatureProcessor processor = factory.createHeaderSignatureProcessor(); + processor.feedHeader(ByteBuffer.wrap(data)); + Header header = new Header<>(); + processor.finish(header); + + verifySignature(true, TestKey.FRESH_EDDSA_KEY, data, header, RpmSignatureTag.DSAHEADER); + } + + + public static Stream factoriesForRsaKey() throws PGPException { + return pgpSignatureProcessorFactoriesForKey(TestKey.MAT, HashAlgorithmTags.SHA256); + } + + @ParameterizedTest + @MethodSource("factoriesForRsaKey") + public void testWithRsaKey(PgpSignatureProcessorFactory factory) throws PGPException, IOException { + byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8); + + SignatureProcessor processor = factory.createHeaderSignatureProcessor(); + processor.feedHeader(ByteBuffer.wrap(data)); + Header header = new Header<>(); + processor.finish(header); + + verifySignature(true, TestKey.MAT, data, header, RpmSignatureTag.RSAHEADER); + } +} diff --git a/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/TestKey.java b/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/TestKey.java new file mode 100644 index 0000000..47d039b --- /dev/null +++ b/pgpainless/src/test/java/org/eclipse/packager/rpm/signature/pgpainless/TestKey.java @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature.pgpainless; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.opentest4j.TestAbortedException; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.key.generation.KeySpec; +import org.pgpainless.key.generation.type.KeyType; +import org.pgpainless.key.generation.type.ecc.EllipticCurve; +import org.pgpainless.key.generation.type.rsa.RsaLength; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.util.Passphrase; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +public abstract class TestKey { + + @Nonnull + public abstract PGPSecretKeyRing getKey(); + + @Nullable + public abstract char[] getPassphrase(); + + public abstract long getPrimaryKeyId(); + + public abstract long getSigningKeyId(); + + public static TestKey MAT = new TestKey() { + @Nonnull + @Override + public PGPSecretKeyRing getKey() { + // The MAT test key is from the test resources directory of the rpm module + File keyFile = new File("../rpm/src/test/resources/key/private_key.txt"); + try ( + FileInputStream fileIn = new FileInputStream(keyFile); + ArmoredInputStream armorIn = new ArmoredInputStream(fileIn); + BCPGInputStream bcIn = new BCPGInputStream(armorIn) + ) { + PGPObjectFactory objectFactory = new BcPGPObjectFactory(bcIn); + return (PGPSecretKeyRing) objectFactory.nextObject(); + } catch (IOException e) { + throw new TestAbortedException("Could not read test key", e); + } + } + + @Nullable + @Override + public char[] getPassphrase() { + return "testkey".toCharArray(); + } + + @Override + public long getPrimaryKeyId() { + return 1757664734257043235L; + } + + @Override + public long getSigningKeyId() { + return getPrimaryKeyId(); + } + }; + + /** + * An OpenPGP key that gets generated on the fly. + * It consists of an EdDSA primary key capable of certification, along with + *
    + *
  • an EdDSA subkey for generating signatures, and
  • + *
  • an XDH subkey for encryption.
  • + *
+ */ + public static TestKey FRESH_EDDSA_KEY = new TestKey() { + + private final PGPSecretKeyRing key; + private final long signingKeyId; + + { + try { + key = PGPainless.generateKeyRing() + .modernKeyRing("Random EdDSA "); + KeyRingInfo info = PGPainless.inspectKeyRing(key); + signingKeyId = info.getSigningSubkeys().get(0).getKeyID(); + } catch (PGPException | InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new RuntimeException("Could not generate fresh EdDSA test key", e); + } + } + + @Nonnull + @Override + public PGPSecretKeyRing getKey() { + return key; + } + + @Nullable + @Override + public char[] getPassphrase() { + return null; + } + + @Override + public long getPrimaryKeyId() { + return key.getPublicKey().getKeyID(); + } + + @Override + public long getSigningKeyId() { + return signingKeyId; + } + }; + + public static TestKey FRESH_ECDSA_KEY = new TestKey() { + + private final PGPSecretKeyRing key; + private final long signingKeyId; + + { + try { + key = PGPainless.buildKeyRing() + .setPrimaryKey(KeySpec.getBuilder(KeyType.ECDSA(EllipticCurve._SECP256K1), KeyFlag.CERTIFY_OTHER)) + .addSubkey(KeySpec.getBuilder(KeyType.ECDSA(EllipticCurve._SECP256K1), KeyFlag.SIGN_DATA)) + .addSubkey(KeySpec.getBuilder(KeyType.ECDH(EllipticCurve._SECP256K1), KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE)) + .addUserId("Random ECDSA ") + .build(); + KeyRingInfo info = PGPainless.inspectKeyRing(key); + signingKeyId = info.getSigningSubkeys().get(0).getKeyID(); + } catch (PGPException | InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new RuntimeException("Could not generate fresh ECDSA test key", e); + } + } + + @Nonnull + @Override + public PGPSecretKeyRing getKey() { + return key; + } + + @Nullable + @Override + public char[] getPassphrase() { + return null; + } + + @Override + public long getPrimaryKeyId() { + return key.getPublicKey().getKeyID(); + } + + @Override + public long getSigningKeyId() { + return signingKeyId; + } + }; + + public static TestKey COMPLEX_RSA_KEY = new TestKey() { + + private final PGPSecretKeyRing key; + private final long signingKeyId; + + { + try { + // RSA key with dedicated certifying primary key, signing subkey and encryption subkey + key = PGPainless.generateKeyRing() + .rsaKeyRing("Complex RSA ", RsaLength._4096, Passphrase.emptyPassphrase()); + KeyRingInfo info = PGPainless.inspectKeyRing(key); + signingKeyId = info.getSigningSubkeys().get(0).getKeyID(); + } catch (PGPException | InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { + throw new RuntimeException("Could not generate complex RSA key", e); + } + } + + @Nonnull + @Override + public PGPSecretKeyRing getKey() { + return key; + } + + @Nullable + @Override + public char[] getPassphrase() { + return null; + } + + @Override + public long getPrimaryKeyId() { + return key.getPublicKey().getKeyID(); + } + + @Override + public long getSigningKeyId() { + return signingKeyId; + } + }; + + public static TestKey COMPLEX_RSA_KEY_MISMATCHED_KEYID = new TestKey() { + @Nonnull + @Override + public PGPSecretKeyRing getKey() { + return COMPLEX_RSA_KEY.getKey(); + } + + @Nullable + @Override + public char[] getPassphrase() { + return COMPLEX_RSA_KEY.getPassphrase(); + } + + @Override + public long getPrimaryKeyId() { + return COMPLEX_RSA_KEY.getPrimaryKeyId(); + } + + @Override + public long getSigningKeyId() { + return COMPLEX_RSA_KEY.getPrimaryKeyId(); // Emulate user-error, primary key is not signing capable + } + }; +} diff --git a/pom.xml b/pom.xml index 15c61ea..c455b4d 100644 --- a/pom.xml +++ b/pom.xml @@ -65,6 +65,7 @@ core deb rpm + pgpainless @@ -80,6 +81,7 @@ 33.0.0-jre 5.8.2 1.5.0 + 1.6.7 2.0.12 1.9 @@ -130,6 +132,11 @@ ${bouncycastle.version} + + org.pgpainless + pgpainless-core + ${pgpainless.version} + org.slf4j slf4j-api diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/RpmSignatureTag.java b/rpm/src/main/java/org/eclipse/packager/rpm/RpmSignatureTag.java index 3ea3338..a3e708e 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/RpmSignatureTag.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/RpmSignatureTag.java @@ -25,7 +25,7 @@ public enum RpmSignatureTag implements RpmBaseTag { SHA256HEADER(273), SIZE(1000), - PGP(1002), + PGP(1002), // RSA MD5(1004), PAYLOAD_SIZE(1007), LONGSIZE(5009); diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/signature/BcPgpSignatureProcessorFactory.java b/rpm/src/main/java/org/eclipse/packager/rpm/signature/BcPgpSignatureProcessorFactory.java new file mode 100644 index 0000000..7fe1915 --- /dev/null +++ b/rpm/src/main/java/org/eclipse/packager/rpm/signature/BcPgpSignatureProcessorFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature; + +import org.bouncycastle.bcpg.HashAlgorithmTags; +import org.bouncycastle.openpgp.PGPPrivateKey; + +/** + * Implementation of the {@link PgpSignatureProcessorFactory} that uses Bouncy Castle directly for signing. + */ +public class BcPgpSignatureProcessorFactory extends PgpSignatureProcessorFactory { + + private final PGPPrivateKey privateKey; + private final int hashAlgorithm; + + public BcPgpSignatureProcessorFactory(PGPPrivateKey privateKey) { + this(privateKey, HashAlgorithmTags.SHA256); + } + + /** + * Create a new factory. + * + * @param privateKey private signing key + * @param hashAlgorithm OpenPgp hash algorithm ID of the digest algorithm used for signing + */ + public BcPgpSignatureProcessorFactory(PGPPrivateKey privateKey, int hashAlgorithm) { + this.privateKey = privateKey; + this.hashAlgorithm = hashAlgorithm; + } + + @Override + public SignatureProcessor createHeaderSignatureProcessor() { + return new RsaHeaderSignatureProcessor(privateKey, hashAlgorithm); + } +} diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/signature/PgpSignatureProcessorFactory.java b/rpm/src/main/java/org/eclipse/packager/rpm/signature/PgpSignatureProcessorFactory.java new file mode 100644 index 0000000..11f59f4 --- /dev/null +++ b/rpm/src/main/java/org/eclipse/packager/rpm/signature/PgpSignatureProcessorFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Paul Schaub + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + */ + +package org.eclipse.packager.rpm.signature; + +/** + * Factory for creating OpenPGP signing-related {@link SignatureProcessor} instances. + * By default, packager will use {@link BcPgpSignatureProcessorFactory}. + * TODO: Use Dependency Injection to allow for dynamic replacing of the factory instance. + */ +public abstract class PgpSignatureProcessorFactory { + + /** + * Create a {@link SignatureProcessor} for signing the header. + * + * @return header signature processor + */ + public abstract SignatureProcessor createHeaderSignatureProcessor(); +} diff --git a/rpm/src/main/java/org/eclipse/packager/rpm/yum/RepositoryCreator.java b/rpm/src/main/java/org/eclipse/packager/rpm/yum/RepositoryCreator.java index 6bb9600..f8622ed 100644 --- a/rpm/src/main/java/org/eclipse/packager/rpm/yum/RepositoryCreator.java +++ b/rpm/src/main/java/org/eclipse/packager/rpm/yum/RepositoryCreator.java @@ -50,7 +50,8 @@ import org.eclipse.packager.rpm.info.RpmInformation; import org.eclipse.packager.rpm.info.RpmInformation.Changelog; import org.eclipse.packager.rpm.info.RpmInformation.Dependency; -import org.eclipse.packager.security.pgp.SigningStream; +import org.eclipse.packager.security.pgp.BcPgpSignerCreator; +import org.eclipse.packager.security.pgp.PgpSignerCreator; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -439,21 +440,23 @@ public Builder setSigning(final Function signingStre return this; } + public Builder setSigning(PgpSignerCreator signingFactory) { + return setSigning(signingFactory.createSigningStream()); + } + + @Deprecated public Builder setSigning(final PGPPrivateKey privateKey) { return setSigning(privateKey, HashAlgorithmTags.SHA1); } + @Deprecated public Builder setSigning(final PGPPrivateKey privateKey, final HashAlgorithm hashAlgorithm) { return setSigning(privateKey, hashAlgorithm.getValue()); } + @Deprecated public Builder setSigning(final PGPPrivateKey privateKey, final int digestAlgorithm) { - if (privateKey != null) { - this.signingStreamCreator = output -> new SigningStream(output, privateKey, digestAlgorithm, false); - } else { - this.signingStreamCreator = null; - } - return this; + return setSigning(new BcPgpSignerCreator(privateKey, digestAlgorithm, false)); } public RepositoryCreator build() {