在循环中测试 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();
}
}
}
}
答案 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();
}
}
}
}