充气城堡:在同一时间范围内(~ 1 秒)多个 PGPS 签名相同

时间:2021-05-29 09:37:58

标签: java rsa bouncycastle sha256 openpgp

在循环中测试 PGPSsignature 的生成时,始终使用相同的输入,我注意到我在很短的时间内获得了相同的签名。

这确实让我感到惊讶:我一直期待签名是不可复制的。

这是有意为之吗?

大约 1 秒后返回不同的签名。

使用的充气城堡包:
bcpg-jdk15on-168.jar
bcprov-jdk15on-168.jar

Java 版本:
openJDK v14.0.2, x64

这里有一个独立的小例子 Proggy 来强调这一点:

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

2 个答案:

答案 0 :(得分:0)

查看org.bouncycastle.bcpg.sig.SignatureCreationTime.timeToBytes()的源代码,了解如何将日期值添加到字节数组中进行签名:

<块引用>
protected static byte[] timeToBytes(
    Date    date)
{
    byte[]    data = new byte[4];
    long        t = date.getTime() / 1000;
    
    data[0] = (byte)(t >> 24);
    data[1] = (byte)(t >> 16);
    data[2] = (byte)(t >> 8);
    data[3] = (byte)t;
    
    return data;
}

如您所见,使用了精确到秒的当前时间(date.getTime() 返回毫秒,/ 1000“删除”毫秒部分)。

看起来这符合 RFC 4880 - 5.9. Literal Data Packet (Tag 11) 部分:

<块引用>
 - A four-octet number that indicates a date associated with the
   literal data.  Commonly, the date might be the modification date
   of a file, or the time the packet was created, or a zero that
   indicates no specific time.

所以它看起来像指定的那样工作。

答案 1 :(得分:0)

默认情况下,PGPSignatureSubpacketGenerator 不包含子数据包。

Creation Time 是 Must 子包,如果在 PGPSignatureSubpacketGenerator 中没有找到,将在 PGPSignatureGenerator#generate() 使用 "Now" 秒。
(正如@Progman 正确指出的那样)

正如我在原始问题的代码中推测的那样,示例代码在 org.bouncycastle.openpgp.examples.ClearSignedFileProcessor 不完美。
现在已修复。见BC Issue #965

我扩展了我的示例代码以添加一个包含纳秒的自定义子数据包, 这会导致每次生成不同的签名:

import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.time.Instant;
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.bcpg.SignatureSubpacket;
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");

    private static final int               CUSTOM_100   = 100;   // (According to RFC 4880, available for use)
    private static final boolean           NON_CRITICAL = false;

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

        setCreationTimeWithCustomNanos      (spGen);

        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);
        });
        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.
         */
    }

    /**
     * Bouncy Castle defaults to use Seconds since the Epoch to set Creation Time.<br>
     * (this is done in {@link PGPSignatureGenerator#generate()}).<br>
     * <br>
     * We set the Creation Time explicitly to {@code "now"}...<br>
     * ...and use the Nanoseconds from {@code "now"} to create a Custom Subpacket.<br>
     * <br>
     * Given the elapsed time necessary to calculate a Signature on contemporary Hardware,
     * this effectively adds a Salt to the deterministic RSA Algorithm,
     * thus making the Signature unique.
     * 
     * @param spGen
     */
    private static void setCreationTimeWithCustomNanos(final PGPSignatureSubpacketGenerator spGen) {

        final Instant nowInstant    = Instant.now();
        final Date    nowTime       = Date.from(nowInstant);

        final int     nowNanos      = nowInstant.getNano();
        final byte[]  nowNanosBytes = new byte[Integer.BYTES];

        ByteBuffer.wrap(nowNanosBytes).putInt(nowNanos);

        spGen.setSignatureCreationTime(                             NON_CRITICAL,        nowTime);
        spGen.addCustomSubpacket(new SignatureSubpacket(CUSTOM_100, NON_CRITICAL, false, nowNanosBytes) {});
    }

    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();
            }
        }
    }
}
相关问题