bcgit / bc-java

Bouncy Castle Java Distribution (Mirror)
https://www.bouncycastle.org/java.html
MIT License
2.32k stars 1.14k forks source link

Multiple PGPSignature's identical within same timeframe (~ 1 second) #965

Closed DanskerDave closed 3 years ago

DanskerDave commented 3 years ago

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 (trivially) reproducible.

Is this behaviour intended?

After roughly (exactly, I presume) 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 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 PgpSimpleSignerBC {

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

        sGen.init(PGPSignature.CANONICAL_TEXT_DOCUMENT, this.privateKey);

        this.publicKey.getUserIDs().forEachRemaining(userID -> {
            spGen.addSignerUserID(false, userID);
        });
        sGen.setHashedSubpackets(spGen.generate());

        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 PgpSimpleSignerBC pgpSimpleSigner = new PgpSimpleSignerBC();

        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();
            }
        }
    }
}
dghgit commented 3 years ago

Yes, you'd expect this. See:

https://github.com/bcgit/bc-java/blob/582954713a7831f589807392f7edc9ccdbc3a4ed/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java#L204

The time resolution used is 1 second. Note that is possible to hard code the creation time. If you do that you will see the same signature block being generated always.

If the question is concerning the actual cryptographic signature - RSA is a deterministic algorithm so the cryptographic signature being generated is always the same. If you want to see the difference with a non-deterministic one try DSA.

DanskerDave commented 3 years ago

ok, thanks for the Info. I experimented a bit with spGen.addCustomSubpacket(new SignatureCreationTime(false, false, customBytes));, adding Nanoseconds cast to int in the customBytes, which introduced a salt, thereby changing the signature. I also tried a Custom SignatureSubpacket : I wanted to add a FileName Tag as mentioned in RFC 4880 §5.9 (Tag 11), but couldn't find the correct Tag-value for "File Name" so just used one of those earmarked for "Private use" (100)

dghgit commented 3 years ago

5.9 is refering to literal data, which is a encapsulation packet for encrypted and signed data. It's used for creating binary data.