While testing generation of a PGPSignature in a loop, always using the same input, I noticed I was getting identical signatures within a short timeframe.
That did rather surprise me: I had been expecting the Signature not to be reproducible.
Is this behaviour intended?
After roughly 1 second a different Signature is returned.
Bouncy Castle Packages used:
bcpg-jdk15on-168.jar
bcprov-jdk15on-168.jar
Java Version:
openJDK v14.0.2, x64
Here's a little self-contained example Proggy to highlight this:
import java.io.IOException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.Iterator;
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags;
import org.bouncycastle.crypto.generators.RSAKeyPairGenerator;
import org.bouncycastle.crypto.params.RSAKeyGenerationParameters;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.bc.BcPGPKeyPair;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
public class PgpSimpleSigner {
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS" + "SSS" + "SSS");
public final PGPPrivateKey privateKey;
public final PGPPublicKey publicKey;
private PgpSimpleSigner() throws IOException, PGPException {
final RSAKeyGenerationParameters kgp = new RSAKeyGenerationParameters(BigInteger.valueOf(0x10001), new SecureRandom(), 2048, 12);
final RSAKeyPairGenerator kpg = new RSAKeyPairGenerator();
kpg.init(kgp);
final PGPKeyPair keyPair = new BcPGPKeyPair(PGPPublicKey.RSA_SIGN, kpg.generateKeyPair(), new Date());
this.privateKey = keyPair.getPrivateKey();
this.publicKey = keyPair.getPublicKey();
}
private PGPSignature sign(final String signMeString) throws Exception {
final int keyAlgorithm = PublicKeyAlgorithmTags.RSA_SIGN;
final int hashAlgorithm = PGPUtil.SHA256;
final JcaPGPContentSignerBuilder csb = new JcaPGPContentSignerBuilder(keyAlgorithm, hashAlgorithm);
csb.setProvider(new BouncyCastleProvider());
final PGPSignatureGenerator sGen = new PGPSignatureGenerator(csb);
final PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
/*
* (spGen contains NO Subpackets, in particular no SignatureCreationTime)
*/
sGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, this.privateKey);
this.publicKey.getUserIDs().forEachRemaining(userID -> {
/*
* Our Test PublicKey has no Users associated, so this Loop is not entered!
*/
spGen.addSignerUserID(false, userID);
/*
* Suspicion: the Example code for this logic in
* org.bouncycastle.openpgp.examples.ClearSignedFileProcessor
* is incorrect? Maybe following should be outside the loop?...
*/
sGen .setHashedSubpackets(spGen.generate()); // never executed!
});
sGen.update(signMeString.getBytes());
return sGen.generate();
/*
* The above logic based on Method
* signFile(String, InputStream, OutputStream, char[], String)
* in
* org.bouncycastle.openpgp.examples.ClearSignedFileProcessor.
*
* ...but without the complicated CR/LF & Whitespace logic
* as we know our input String is RFC 4880 compliant.
*/
}
public static void main(final String[] args) throws Throwable {
final PgpSimpleSigner pgpSimpleSigner = new PgpSimpleSigner();
byte[] bcSigBytesPrev = {};
long t0 = System.nanoTime();
while (true) {
final long nsSinceDelta = System.nanoTime() - t0;
final PGPSignature bcSig = pgpSimpleSigner.sign("Sign me, I'm RFC 4880 compliant");
final byte[] bcSigBytes = bcSig.getSignature();
if (Arrays.compare(bcSigBytesPrev, bcSigBytes) != 0) {
bcSigBytesPrev = bcSigBytes;
System.out.println(FMT.format(ZonedDateTime.now()) + "\t" + nsSinceDelta + "\t" + Base64.getEncoder().encodeToString(bcSigBytes));
t0 = System.nanoTime();
}
}
}
}