diff --git a/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyPacket.java b/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyPacket.java index 0fbe12c2b9..55d1396fa9 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyPacket.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/PublicKeyPacket.java @@ -13,6 +13,7 @@ public class PublicKeyPacket { public static final int VERSION_3 = 3; public static final int VERSION_4 = 4; + public static final int LIBREPGP_5 = 5; public static final int VERSION_6 = 6; private int version; @@ -43,66 +44,106 @@ public class PublicKeyPacket this(keyTag, in, false); } + /** + * Parse a {@link PublicKeyPacket} or {@link PublicSubkeyPacket} from an OpenPGP {@link BCPGInputStream}. + * If
packetTypeIDis {@link #PUBLIC_KEY}, the packet is a primary key. + * If instead it is {@link #PUBLIC_SUBKEY}, it is a subkey packet. + * If
newPacketFormatis true, the packet format is remembered as {@link PacketFormat#CURRENT}, + * otherwise as {@link PacketFormat#LEGACY}. + * @param packetTypeID packet type ID + * @param in packet input stream + * @param newPacketFormat packet format + * @throws IOException if the key packet cannot be parsed + * + * @see + * C-R - Version 3 Public Keys + * @see + * C-R - Version 4 Public Keys + * @see + * C-R - Version 6 Public Keys + * @see + * LibrePGP - Public-Key Packet Formats + */ PublicKeyPacket( - int keyTag, + int packetTypeID, BCPGInputStream in, boolean newPacketFormat) throws IOException { - super(keyTag, newPacketFormat); + super(packetTypeID, newPacketFormat); version = in.read(); - time = ((long)in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read(); - if (version <= VERSION_3) + if (version < 2 || version > VERSION_6) + { + throw new UnsupportedPacketVersionException("Unsupported Public Key Packet version encountered: " + version); + } + + time = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read(); + + if (version == 2 || version == VERSION_3) { validDays = (in.read() << 8) | in.read(); } - algorithm = (byte)in.read(); - if (version == VERSION_6) + algorithm = (byte) in.read(); + + long keyOctetCount = -1; + if (version == LIBREPGP_5 || version == VERSION_6) { - // TODO: Use keyOctets to be able to parse unknown keys - long keyOctets = ((long)in.read() << 24) | ((long)in.read() << 16) | ((long)in.read() << 8) | in.read(); + keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read(); } - switch (algorithm) + parseKey(in, algorithm, keyOctetCount); + } + + /** + * Parse algorithm-specific public key material. + * @param in input stream which read just up to the public key material + * @param algorithmId public key algorithm ID + * @param optLen optional: Length of the public key material. -1 if not present. + * @throws IOException if the pk material cannot be parsed + */ + private void parseKey(BCPGInputStream in, int algorithmId, long optLen) + throws IOException + { + switch (algorithmId) { - case RSA_ENCRYPT: - case RSA_GENERAL: - case RSA_SIGN: - key = new RSAPublicBCPGKey(in); - break; - case DSA: - key = new DSAPublicBCPGKey(in); - break; - case ELGAMAL_ENCRYPT: - case ELGAMAL_GENERAL: - key = new ElGamalPublicBCPGKey(in); - break; - case ECDH: - key = new ECDHPublicBCPGKey(in); - break; - case X25519: - key = new X25519PublicBCPGKey(in); - break; - case X448: - key = new X448PublicBCPGKey(in); - break; - case ECDSA: - key = new ECDSAPublicBCPGKey(in); - break; - case EDDSA_LEGACY: - key = new EdDSAPublicBCPGKey(in); - break; - case Ed25519: - key = new Ed25519PublicBCPGKey(in); - break; - case Ed448: - key = new Ed448PublicBCPGKey(in); - break; - default: - throw new IOException("unknown PGP public key algorithm encountered: " + algorithm); + case RSA_ENCRYPT: + case RSA_GENERAL: + case RSA_SIGN: + key = new RSAPublicBCPGKey(in); + break; + case DSA: + key = new DSAPublicBCPGKey(in); + break; + case ELGAMAL_ENCRYPT: + case ELGAMAL_GENERAL: + key = new ElGamalPublicBCPGKey(in); + break; + case ECDH: + key = new ECDHPublicBCPGKey(in); + break; + case X25519: + key = new X25519PublicBCPGKey(in); + break; + case X448: + key = new X448PublicBCPGKey(in); + break; + case ECDSA: + key = new ECDSAPublicBCPGKey(in); + break; + case EDDSA_LEGACY: + key = new EdDSAPublicBCPGKey(in); + break; + case Ed25519: + key = new Ed25519PublicBCPGKey(in); + break; + case Ed448: + key = new Ed448PublicBCPGKey(in); + break; + default: + throw new IOException("unknown PGP public key algorithm encountered: " + algorithm); } } @@ -184,7 +225,7 @@ public byte[] getEncodedContents() pOut.write(algorithm); - if (version == VERSION_6) + if (version == VERSION_6 || version == LIBREPGP_5) { int keyOctets = key.getEncoded().length; pOut.write(keyOctets >> 24); diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java index 198d3333b0..145eb4adca 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/SecretKeyPacket.java @@ -1,5 +1,6 @@ package org.bouncycastle.bcpg; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -96,16 +97,30 @@ public class SecretKeyPacket } /** - * @param in - * @throws IOException + * Parse a {@link SecretKeyPacket} or {@link SecretSubkeyPacket} from an OpenPGP {@link BCPGInputStream}. + * The return type depends on the
packetTypeID: + * {@link PacketTags#SECRET_KEY} means the result is a {@link SecretKeyPacket}. + * {@link PacketTags#SECRET_SUBKEY} results in a {@link SecretSubkeyPacket}. + * + * @param packetTypeID packet type ID + * @param in packet input stream + * @param newPacketFormat packet format + * @throws IOException if the secret key packet cannot be parsed + * + * @see + * C-R - Secret-Key Packet Formats + * @see + * LibrePGP - Secret-Key Packet Formats + * @see + * Hardware-Backed Secret Keys */ SecretKeyPacket( - int keyTag, + int packetTypeID, BCPGInputStream in, boolean newPacketFormat) throws IOException { - super(keyTag, newPacketFormat); + super(packetTypeID, newPacketFormat); if (this instanceof SecretSubkeyPacket) { @@ -119,12 +134,15 @@ public class SecretKeyPacket int version = pubKeyPacket.getVersion(); s2kUsage = in.read(); - if (version == 6 && s2kUsage != USAGE_NONE) + int conditionalParameterLength = -1; + if (version == PublicKeyPacket.LIBREPGP_5 || + (version == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE)) { // TODO: Use length to parse unknown parameters - int conditionalParameterLength = in.read(); + conditionalParameterLength = in.read(); } + // 255, 254, 253 if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD) { encAlgorithm = in.read(); @@ -133,44 +151,74 @@ public class SecretKeyPacket { encAlgorithm = s2kUsage; } + + // 253 if (s2kUsage == USAGE_AEAD) { aeadAlgorithm = in.read(); } - if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD) + + // version = 6 && 254 || 253 + if (version == PublicKeyPacket.VERSION_6 && (s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD)) { - if (version == PublicKeyPacket.VERSION_6) + int s2KLen = in.read(); + byte[] s2kBytes = new byte[s2KLen]; + in.readFully(s2kBytes); + + // TODO: catch UnsupportedPacketVersionException gracefully + s2k = new S2K(new ByteArrayInputStream(s2kBytes)); + } + else + { + // 255, 254, 253 + if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_SHA1 || s2kUsage == USAGE_AEAD) { - // TODO: Use length to parse unknown S2Ks - int s2kLen = in.read(); + s2k = new S2K(in); } - s2k = new S2K(in); } + if (s2kUsage == USAGE_AEAD) { iv = new byte[AEADUtils.getIVLength(aeadAlgorithm)]; Streams.readFully(in, iv); } - boolean isGNUDummyNoPrivateKey = s2k != null + else + { + boolean isGNUDummyNoPrivateKey = s2k != null && s2k.getType() == S2K.GNU_DUMMY_S2K && s2k.getProtectionMode() == S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY; - if (!(isGNUDummyNoPrivateKey)) - { - if (s2kUsage != 0 && iv == null) + if (!(isGNUDummyNoPrivateKey)) { - if (encAlgorithm < 7) + if (s2kUsage != USAGE_NONE && iv == null) { - iv = new byte[8]; + if (encAlgorithm < 7) + { + iv = new byte[8]; + } + else + { + iv = new byte[16]; + } + in.readFully(iv, 0, iv.length); } - else - { - iv = new byte[16]; - } - in.readFully(iv, 0, iv.length); } } - this.secKeyData = in.readAll(); + if (version == PublicKeyPacket.LIBREPGP_5) + { + long keyOctetCount = ((long) in.read() << 24) | ((long) in.read() << 16) | ((long) in.read() << 8) | in.read(); + if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_NONE) + { + // encoded keyOctetCount does not contain checksum + keyOctetCount += 2; + } + this.secKeyData = new byte[(int) keyOctetCount]; + in.readFully(secKeyData); + } + else + { + this.secKeyData = in.readAll(); + } } /** @@ -291,9 +339,11 @@ public byte[] getEncodedContents() pOut.write(s2kUsage); + // conditional parameters byte[] conditionalParameters = encodeConditionalParameters(); - if (pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE) + if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5 || + (pubKeyPacket.getVersion() == PublicKeyPacket.VERSION_6 && s2kUsage != USAGE_NONE)) { pOut.write(conditionalParameters.length); } @@ -302,9 +352,21 @@ public byte[] getEncodedContents() // encrypted secret key if (secKeyData != null && secKeyData.length > 0) { + if (pubKeyPacket.getVersion() == PublicKeyPacket.LIBREPGP_5) + { + int keyOctetCount = secKeyData.length; + // v5 keyOctetCount does not include checksum octets + if (s2kUsage == USAGE_CHECKSUM || s2kUsage == USAGE_NONE) + { + keyOctetCount -= 2; + } + pOut.write(keyOctetCount >> 24); + pOut.write(keyOctetCount >> 16); + pOut.write(keyOctetCount >> 8); + pOut.write(keyOctetCount); + } pOut.write(secKeyData); } - pOut.close(); return bOut.toByteArray(); diff --git a/pg/src/main/java/org/bouncycastle/bcpg/SignaturePacket.java b/pg/src/main/java/org/bouncycastle/bcpg/SignaturePacket.java index d716dcdae7..939a0d24b8 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/SignaturePacket.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/SignaturePacket.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.util.Vector; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; import org.bouncycastle.bcpg.sig.IssuerKeyID; import org.bouncycastle.bcpg.sig.SignatureCreationTime; import org.bouncycastle.util.Arrays; @@ -240,6 +241,9 @@ else if (p instanceof SignatureCreationTime) unhashedData[i] = p; } + + setIssuerKeyId(); + setCreationTime(); } /** @@ -398,7 +402,8 @@ public SignaturePacket( SignatureSubpacket[] hashedData, SignatureSubpacket[] unhashedData, byte[] fingerPrint, - byte[] signatureEncoding) + byte[] signatureEncoding, + byte[] salt) { super(SIGNATURE); @@ -411,6 +416,37 @@ public SignaturePacket( this.unhashedData = unhashedData; this.fingerPrint = fingerPrint; this.signatureEncoding = Arrays.clone(signatureEncoding); + this.salt = Arrays.clone(salt); + if (hashedData != null) + { + setCreationTime(); + } + } + + public SignaturePacket( + int version, + int signatureType, + long keyID, + int keyAlgorithm, + int hashAlgorithm, + SignatureSubpacket[] hashedData, + SignatureSubpacket[] unhashedData, + byte[] fingerPrint, + MPInteger[] signature, + byte[] salt) + { + super(SIGNATURE); + + this.version = version; + this.signatureType = signatureType; + this.keyID = keyID; + this.keyAlgorithm = keyAlgorithm; + this.hashAlgorithm = hashAlgorithm; + this.hashedData = hashedData; + this.unhashedData = unhashedData; + this.fingerPrint = fingerPrint; + this.signature = signature; + this.salt = Arrays.clone(salt); if (hashedData != null) { setCreationTime(); @@ -471,7 +507,7 @@ public byte[] getSignatureTrailer() { byte[] trailer = null; - if (version == 3 || version == 2) + if (version == VERSION_3 || version == VERSION_2) { trailer = new byte[5]; @@ -483,7 +519,7 @@ public byte[] getSignatureTrailer() trailer[3] = (byte)(time >> 8); trailer[4] = (byte)(time); } - else + else if (version == VERSION_4 || version == VERSION_5 || version == VERSION_6) { ByteArrayOutputStream sOut = new ByteArrayOutputStream(); SignatureSubpacket[] hashed = this.getHashedSubPackets(); @@ -503,14 +539,29 @@ public byte[] getSignatureTrailer() } byte[] data = hOut.toByteArray(); - StreamUtil.write2OctetLength(sOut, data.length); + if (version != VERSION_6) + { + StreamUtil.write2OctetLength(sOut, data.length); + } + else + { + StreamUtil.write4OctetLength(sOut, data.length); + } sOut.write(data); byte[] hData = sOut.toByteArray(); sOut.write((byte)this.getVersion()); sOut.write((byte)0xff); - StreamUtil.write4OctetLength(sOut, hData.length); + + if (version == VERSION_5) + { + StreamUtil.write8OctetLength(sOut, hData.length); + } + else + { + StreamUtil.write4OctetLength(sOut, hData.length); + } } catch (IOException e) { @@ -709,6 +760,49 @@ private void setCreationTime() } } + /** + * Iterate over the hashed and unhashed signature subpackets to identify either a {@link IssuerKeyID} or + * {@link IssuerFingerprint} subpacket to derive the issuer key-ID from. + * The issuer {@link IssuerKeyID} and {@link IssuerFingerprint} subpacket information is "self-authenticating", + * as its authenticity can be verified by checking the signature with the corresponding key. + * Therefore, we can also check the unhashed signature subpacket area. + */ + private void setIssuerKeyId() + { + if (keyID != 0L) + { + return; + } + + for (SignatureSubpacket p : hashedData) + { + if (p instanceof IssuerKeyID) + { + keyID = ((IssuerKeyID) p).getKeyID(); + return; + } + if (p instanceof IssuerFingerprint) + { + keyID = ((IssuerFingerprint) p).getKeyID(); + return; + } + } + + for (SignatureSubpacket p : unhashedData) + { + if (p instanceof IssuerKeyID) + { + keyID = ((IssuerKeyID) p).getKeyID(); + return; + } + if (p instanceof IssuerFingerprint) + { + keyID = ((IssuerFingerprint) p).getKeyID(); + return; + } + } + } + public static SignaturePacket fromByteArray(byte[] data) throws IOException { diff --git a/pg/src/main/java/org/bouncycastle/bcpg/StreamUtil.java b/pg/src/main/java/org/bouncycastle/bcpg/StreamUtil.java index c91961cce0..5f4afe30c1 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/StreamUtil.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/StreamUtil.java @@ -144,9 +144,35 @@ static void write4OctetLength(OutputStream pOut, int len) } static int read4OctetLength(InputStream in) - throws IOException + throws IOException { return (in.read() << 24) | (in.read() << 16) | (in.read() << 8) | in.read(); } + static void write8OctetLength(OutputStream pOut, long len) + throws IOException + { + pOut.write((int) (len >> 56)); + pOut.write((int) (len >> 48)); + pOut.write((int) (len >> 40)); + pOut.write((int) (len >> 32)); + pOut.write((int) (len >> 24)); + pOut.write((int) (len >> 16)); + pOut.write((int) (len >> 8)); + pOut.write((int) len); + } + + static long read8OctetLength(InputStream in) + throws IOException + { + return ((long) in.read() << 56) | + ((long) in.read() << 48) | + ((long) in.read() << 40) | + ((long) in.read() << 32) | + ((long) in.read() << 24) | + ((long) in.read() << 16) | + ((long) in.read() << 8) | + ((long) in.read()); + } + } diff --git a/pg/src/main/java/org/bouncycastle/bcpg/sig/IssuerFingerprint.java b/pg/src/main/java/org/bouncycastle/bcpg/sig/IssuerFingerprint.java index 8432acb5e7..e294a10fb2 100644 --- a/pg/src/main/java/org/bouncycastle/bcpg/sig/IssuerFingerprint.java +++ b/pg/src/main/java/org/bouncycastle/bcpg/sig/IssuerFingerprint.java @@ -1,5 +1,7 @@ package org.bouncycastle.bcpg.sig; +import org.bouncycastle.bcpg.FingerprintUtil; +import org.bouncycastle.bcpg.PublicKeyPacket; import org.bouncycastle.bcpg.SignatureSubpacket; import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.util.Arrays; @@ -36,4 +38,21 @@ public byte[] getFingerprint() { return Arrays.copyOfRange(data, 1, data.length); } + + public long getKeyID() + { + if (getKeyVersion() == PublicKeyPacket.VERSION_4) + { + return FingerprintUtil.keyIdFromV4Fingerprint(getFingerprint()); + } + if (getKeyVersion() == PublicKeyPacket.LIBREPGP_5) + { + return FingerprintUtil.keyIdFromLibrePgpFingerprint(getFingerprint()); + } + if (getKeyVersion() == PublicKeyPacket.VERSION_6) + { + return FingerprintUtil.keyIdFromV6Fingerprint(getFingerprint()); + } + return 0; + } } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPDefaultSignatureGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPDefaultSignatureGenerator.java index bc544d059f..c5bfec5b40 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPDefaultSignatureGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPDefaultSignatureGenerator.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.OutputStream; +import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.bcpg.UserAttributeSubpacket; abstract class PGPDefaultSignatureGenerator @@ -12,6 +13,13 @@ abstract class PGPDefaultSignatureGenerator protected OutputStream sigOut; protected int sigType; + protected final int version; + + public PGPDefaultSignatureGenerator(int version) + { + this.version = version; + } + public void update( byte b) { @@ -108,9 +116,28 @@ protected void updateWithPublicKey(PGPPublicKey key) { byte[] keyBytes = getEncodedPublicKey(key); - this.update((byte)0x99); - this.update((byte)(keyBytes.length >> 8)); - this.update((byte)(keyBytes.length)); + if (version == SignaturePacket.VERSION_4) + { + this.update((byte) 0x99); + this.update((byte) (keyBytes.length >> 8)); + this.update((byte) (keyBytes.length)); + } + else if (version == SignaturePacket.VERSION_5) + { + this.update((byte) 0x9A); + this.update((byte) (keyBytes.length >> 24)); + this.update((byte) (keyBytes.length >> 16)); + this.update((byte) (keyBytes.length >> 8)); + this.update((byte) (keyBytes.length)); + } + else if (version == SignaturePacket.VERSION_6) + { + this.update((byte) 0x9B); + this.update((byte) (keyBytes.length >> 24)); + this.update((byte) (keyBytes.length >> 16)); + this.update((byte) (keyBytes.length >> 8)); + this.update((byte) (keyBytes.length)); + } this.update(keyBytes); } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java index 5f6aa5f853..029139359f 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPOnePassSignature.java @@ -6,11 +6,14 @@ import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashUtils; import org.bouncycastle.bcpg.OnePassSignaturePacket; import org.bouncycastle.bcpg.Packet; +import org.bouncycastle.bcpg.SignaturePacket; import org.bouncycastle.openpgp.operator.PGPContentVerifier; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilder; import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider; +import org.bouncycastle.util.Arrays; /** * A one pass signature object. @@ -41,6 +44,8 @@ public PGPOnePassSignature( PGPOnePassSignature( OnePassSignaturePacket sigPack) { + // v3 OPSs are typically used with v4 sigs + super(sigPack.getVersion() == OnePassSignaturePacket.VERSION_3 ? SignaturePacket.VERSION_4 : sigPack.getVersion()); this.sigPack = sigPack; this.sigType = sigPack.getSignatureType(); } @@ -61,6 +66,41 @@ public void init(PGPContentVerifierBuilderProvider verifierBuilderProvider, PGPP lastb = 0; sigOut = verifier.getOutputStream(); + + checkSaltSize(); + updateWithSalt(); + } + + private void checkSaltSize() + throws PGPException + { + if (getVersion() != SignaturePacket.VERSION_6) + { + return; + } + + int expectedSaltSize = HashUtils.getV6SignatureSaltSizeInBytes(getHashAlgorithm()); + if (expectedSaltSize != getSalt().length) + { + throw new PGPException("RFC9580 defines the salt size for " + PGPUtil.getDigestName(getHashAlgorithm()) + + " as " + expectedSaltSize + " octets, but signature has " + getSalt().length + " octets."); + } + } + + private void updateWithSalt() + throws PGPException + { + if (version == SignaturePacket.VERSION_6) + { + try + { + sigOut.write(getSalt()); + } + catch (IOException e) + { + throw new PGPException("Cannot salt the signature.", e); + } + } } /** @@ -74,6 +114,8 @@ public boolean verify( PGPSignature pgpSig) throws PGPException { + compareSalt(pgpSig); + try { sigOut.write(pgpSig.getSignatureTrailer()); @@ -88,6 +130,19 @@ public boolean verify( return verifier.verify(pgpSig.getSignature()); } + private void compareSalt(PGPSignature signature) + throws PGPException + { + if (version != SignaturePacket.VERSION_6) + { + return; + } + if (!Arrays.constantTimeAreEqual(getSalt(), signature.getSalt())) + { + throw new PGPException("Salt in OnePassSignaturePacket does not match salt in SignaturePacket."); + } + } + /** * Return the packet version. * diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java index 1c2a5ecebc..b5060ef848 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignature.java @@ -12,6 +12,7 @@ import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.HashUtils; import org.bouncycastle.bcpg.MPInteger; import org.bouncycastle.bcpg.Packet; import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; @@ -76,6 +77,7 @@ public PGPSignature( PGPSignature( PGPSignature signature) { + super(signature.getVersion()); sigPck = signature.sigPck; sigType = signature.sigType; trustPck = signature.trustPck; @@ -91,6 +93,7 @@ public PGPSignature( SignaturePacket sigPacket, TrustPacket trustPacket) { + super(sigPacket.getVersion()); this.sigPck = sigPacket; this.sigType = sigPck.getSignatureType(); this.trustPck = trustPacket; @@ -161,10 +164,46 @@ PGPContentVerifierBuilder createVerifierProvider(PGPContentVerifierBuilderProvid } void init(PGPContentVerifier verifier) + throws PGPException { this.verifier = verifier; this.lastb = 0; this.sigOut = verifier.getOutputStream(); + + checkSaltSize(); + updateWithSalt(); + } + + private void checkSaltSize() + throws PGPException + { + if (getVersion() != SignaturePacket.VERSION_6) + { + return; + } + + int expectedSaltSize = HashUtils.getV6SignatureSaltSizeInBytes(getHashAlgorithm()); + if (expectedSaltSize != sigPck.getSalt().length) + { + throw new PGPException("RFC9580 defines the salt size for " + PGPUtil.getDigestName(getHashAlgorithm()) + + " as " + expectedSaltSize + " octets, but signature has " + sigPck.getSalt().length + " octets."); + } + } + + private void updateWithSalt() + throws PGPException + { + if (getVersion() == SignaturePacket.VERSION_6) + { + try + { + sigOut.write(sigPck.getSalt()); + } + catch (IOException e) + { + throw new PGPException("Cannot update signature with salt.", e); + } + } } public boolean verify() @@ -439,6 +478,11 @@ private PGPSignatureSubpacketVector createSubpacketVector(SignatureSubpacket[] p return null; } + byte[] getSalt() + { + return sigPck.getSalt(); + } + public byte[] getSignature() throws PGPException { diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java index 3f34aaac27..4ff45e966a 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPSignatureGenerator.java @@ -29,18 +29,70 @@ public class PGPSignatureGenerator private PGPContentSignerBuilder contentSignerBuilder; private PGPContentSigner contentSigner; private int providedKeyAlgorithm = -1; + private PGPPublicKey signingPubKey; /** - * Create a signature generator built on the passed in contentSignerBuilder. + * Create a version 4 signature generator built on the passed in contentSignerBuilder. * * @param contentSignerBuilder builder to produce PGPContentSigner objects for generating signatures. + * @deprecated use {@link #PGPSignatureGenerator(PGPContentSignerBuilder, PGPPublicKey)} instead. */ public PGPSignatureGenerator( PGPContentSignerBuilder contentSignerBuilder) { + this(contentSignerBuilder, SignaturePacket.VERSION_4); + } + + /** + * Create a signature generator built on the passed in contentSignerBuilder. + * + * @param contentSignerBuilder builder to produce PGPContentSigner objects for generating signatures. + * @param version signature version + */ + PGPSignatureGenerator( + PGPContentSignerBuilder contentSignerBuilder, + int version) + { + super(version); this.contentSignerBuilder = contentSignerBuilder; } + /** + * Create a signature generator built on the passed in contentSignerBuilder. + * The produced signature version will match the version of the passed in signing key. + * + * @param contentSignerBuilder builder to produce PGPContentSigner objects for generating signatures + * @param signingKey signing key + */ + public PGPSignatureGenerator( + PGPContentSignerBuilder contentSignerBuilder, + PGPPublicKey signingKey) + { + this(contentSignerBuilder, signingKey, signingKey.getVersion()); + } + + /** + * Create a signature generator built on the passed in contentSignerBuilder. + * The signature that is being produced will match the passed in signature version. + * NOTE: You cannot use a v6 signing key to produce signatures of any other version than 6. + * + * @param contentSignerBuilder builder to produce PGPContentSigner objects for generating signatures + * @param signingKey signing key + * @param signatureVersion version of the produced signature packet + */ + public PGPSignatureGenerator( + PGPContentSignerBuilder contentSignerBuilder, + PGPPublicKey signingKey, + int signatureVersion) + { + this(contentSignerBuilder, signatureVersion); + this.signingPubKey = signingKey; + if (signingKey.getVersion() == 6 && signatureVersion != 6) + { + throw new IllegalArgumentException("Version 6 keys MUST only generate version 6 signatures."); + } + } + /** * Initialise the generator for signing. * @@ -212,7 +264,7 @@ else if (contentSigner.getKeyAlgorithm() == PublicKeyAlgorithmTags.Ed25519 || { // Ed25519, Ed448 use raw encoding instead of MPI return new PGPSignature(new SignaturePacket(4, sigType, contentSigner.getKeyID(), contentSigner.getKeyAlgorithm(), - contentSigner.getHashAlgorithm(), hPkts, unhPkts, fingerPrint, contentSigner.getSignature())); + contentSigner.getHashAlgorithm(), hPkts, unhPkts, fingerPrint, contentSigner.getSignature(), null)); } } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/PGPV3SignatureGenerator.java b/pg/src/main/java/org/bouncycastle/openpgp/PGPV3SignatureGenerator.java index c743ce901c..0396e3648f 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/PGPV3SignatureGenerator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/PGPV3SignatureGenerator.java @@ -29,6 +29,7 @@ public class PGPV3SignatureGenerator public PGPV3SignatureGenerator( PGPContentSignerBuilder contentSignerBuilder) { + super(SignaturePacket.VERSION_3); this.contentSignerBuilder = contentSignerBuilder; } diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcKeyFingerprintCalculator.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcKeyFingerprintCalculator.java index 11ec4901a2..06b89ee052 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcKeyFingerprintCalculator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/bc/BcKeyFingerprintCalculator.java @@ -23,7 +23,7 @@ public byte[] calculateFingerprint(PublicKeyPacket publicPk) BCPGKey key = publicPk.getKey(); Digest digest; - if (publicPk.getVersion() <= 3) + if (publicPk.getVersion() <= PublicKeyPacket.VERSION_3) { RSAPublicBCPGKey rK = (RSAPublicBCPGKey)key; @@ -42,7 +42,7 @@ public byte[] calculateFingerprint(PublicKeyPacket publicPk) throw new PGPException("can't encode key components: " + e.getMessage(), e); } } - else if (publicPk.getVersion() == 4) + else if (publicPk.getVersion() == PublicKeyPacket.VERSION_4) { try { @@ -60,14 +60,14 @@ else if (publicPk.getVersion() == 4) throw new PGPException("can't encode key components: " + e.getMessage(), e); } } - else if (publicPk.getVersion() == 6) + else if (publicPk.getVersion() == 5 || publicPk.getVersion() == PublicKeyPacket.VERSION_6) { try { byte[] kBytes = publicPk.getEncodedContents(); digest = new SHA256Digest(); - digest.update((byte)0x9b); + digest.update((byte) (publicPk.getVersion() == PublicKeyPacket.VERSION_6 ? 0x9b : 0x9a)); digest.update((byte)(kBytes.length >> 24)); digest.update((byte)(kBytes.length >> 16)); diff --git a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaKeyFingerprintCalculator.java b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaKeyFingerprintCalculator.java index fab2ba5d9d..29185d9ddd 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaKeyFingerprintCalculator.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/operator/jcajce/JcaKeyFingerprintCalculator.java @@ -63,7 +63,7 @@ public byte[] calculateFingerprint(PublicKeyPacket publicPk) { BCPGKey key = publicPk.getKey(); - if (publicPk.getVersion() <= 3) + if (publicPk.getVersion() <= PublicKeyPacket.VERSION_3) { RSAPublicBCPGKey rK = (RSAPublicBCPGKey)key; @@ -92,7 +92,7 @@ public byte[] calculateFingerprint(PublicKeyPacket publicPk) throw new PGPException("can't encode key components: " + e.getMessage(), e); } } - else if (publicPk.getVersion() == 4) + else if (publicPk.getVersion() == PublicKeyPacket.VERSION_4) { try { @@ -120,15 +120,14 @@ else if (publicPk.getVersion() == 4) throw new PGPException("can't encode key components: " + e.getMessage(), e); } } - else if (publicPk.getVersion() == 6) + else if (publicPk.getVersion() == 5 || publicPk.getVersion() == PublicKeyPacket.VERSION_6) { try { byte[] kBytes = publicPk.getEncodedContents(); MessageDigest digest = helper.createMessageDigest("SHA-256"); - - digest.update((byte)0x9b); + digest.update((byte) (publicPk.getVersion() == PublicKeyPacket.VERSION_6 ? 0x9b : 0x9a)); digest.update((byte)(kBytes.length >> 24)); digest.update((byte)(kBytes.length >> 16)); diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/OperatorBcTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/OperatorBcTest.java index 88d85b1a2e..5fc4f26f24 100644 --- a/pg/src/test/java/org/bouncycastle/openpgp/test/OperatorBcTest.java +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/OperatorBcTest.java @@ -145,12 +145,13 @@ public void testBcKeyFingerprintCalculator() KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", "BC"); kpGen.initialize(1024); KeyPair kp = kpGen.generateKeyPair(); + Date creationTime = new Date(1000 * (new Date().getTime() / 1000)); JcaPGPKeyConverter converter = new JcaPGPKeyConverter().setProvider(new BouncyCastleProvider()); - final PGPPublicKey pubKey = converter.getPGPPublicKey(PublicKeyAlgorithmTags.RSA_GENERAL, kp.getPublic(), new Date()); + final PGPPublicKey pubKey = converter.getPGPPublicKey(PublicKeyAlgorithmTags.RSA_GENERAL, kp.getPublic(), creationTime); - PublicKeyPacket pubKeyPacket = new PublicKeyPacket(6, PublicKeyAlgorithmTags.RSA_GENERAL, new Date(), pubKey.getPublicKeyPacket().getKey()); - byte[] output = calculator.calculateFingerprint(new PublicKeyPacket(6, PublicKeyAlgorithmTags.RSA_GENERAL, new Date(), pubKey.getPublicKeyPacket().getKey())); + PublicKeyPacket pubKeyPacket = new PublicKeyPacket(6, PublicKeyAlgorithmTags.RSA_GENERAL, creationTime, pubKey.getPublicKeyPacket().getKey()); + byte[] output = calculator.calculateFingerprint(new PublicKeyPacket(6, PublicKeyAlgorithmTags.RSA_GENERAL, creationTime, pubKey.getPublicKeyPacket().getKey())); byte[] kBytes = pubKeyPacket.getEncodedContents(); SHA256Digest digest = new SHA256Digest(); @@ -167,16 +168,24 @@ public void testBcKeyFingerprintCalculator() digest.doFinal(digBuf, 0); isTrue(areEqual(output, digBuf)); - final PublicKeyPacket pubKeyPacket2 = new PublicKeyPacket(5, PublicKeyAlgorithmTags.RSA_GENERAL, new Date(), pubKey.getPublicKeyPacket().getKey()); - testException("Unsupported PGP key version: ", "UnsupportedPacketVersionException", new TestExceptionOperation() - { - @Override - public void operation() - throws Exception - { - calculator.calculateFingerprint(pubKeyPacket2); - } - }); + final PublicKeyPacket pubKeyPacket2 = new PublicKeyPacket(5, PublicKeyAlgorithmTags.RSA_GENERAL, creationTime, pubKey.getPublicKeyPacket().getKey()); + kBytes = pubKeyPacket2.getEncodedContents(); + output = calculator.calculateFingerprint(pubKeyPacket2); + + digest = new SHA256Digest(); + + digest.update((byte)0x9a); + + digest.update((byte)(kBytes.length >> 24)); + digest.update((byte)(kBytes.length >> 16)); + digest.update((byte)(kBytes.length >> 8)); + digest.update((byte)kBytes.length); + + digest.update(kBytes, 0, kBytes.length); + digBuf = new byte[digest.getDigestSize()]; + + digest.doFinal(digBuf, 0); + isTrue(areEqual(output, digBuf)); } // public void testBcPBESecretKeyDecryptorBuilder() diff --git a/pg/src/test/java/org/bouncycastle/openpgp/test/PGPV5KeyTest.java b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPV5KeyTest.java new file mode 100644 index 0000000000..51d19e2fc9 --- /dev/null +++ b/pg/src/test/java/org/bouncycastle/openpgp/test/PGPV5KeyTest.java @@ -0,0 +1,148 @@ +package org.bouncycastle.openpgp.test; + +import org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.bcpg.BCPGInputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.PacketFormat; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPObjectFactory; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.bc.BcPGPObjectFactory; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.io.Streams; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +public class PGPV5KeyTest + extends AbstractPgpKeyPairTest +{ + + @Override + public String getName() + { + return "PGPV5KeyTest"; + } + + @Override + public void performTest() + throws Exception + { + parseAndEncodeKey(); + parseCertificateAndVerifyKeySigs(); + } + + private void parseAndEncodeKey() + throws IOException + { + String KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "\n" + + "lGEFXJH05BYAAAAtCSsGAQQB2kcPAQEHQFhZlVcVVtwf+21xNQPX+ecMJJBL0MPd\n" + + "fj75iux+my8QAAAAAAAiAQCHZ1SnSUmWqxEsoI6facIVZQu6mph3cBFzzTvcm5lA\n" + + "Ng5ctBhlbW1hLmdvbGRtYW5AZXhhbXBsZS5uZXSIlgUTFggASCIhBRk0e8mHJGQC\n" + + "X5nfPsLgAA7ZiEiS4fez6kyUAJFZVptUBQJckfTkAhsDBQsJCAcCAyICAQYVCgkI\n" + + "CwIEFgIDAQIeBwIXgAAA9cAA/jiR3yMsZMeEQ40u6uzEoXa6UXeV/S3wwJAXRJy9\n" + + "M8s0AP9vuL/7AyTfFXwwzSjDnYmzS0qAhbLDQ643N+MXGBJ2BZxmBVyR9OQSAAAA\n" + + "MgorBgEEAZdVAQUBAQdA+nysrzml2UCweAqtpDuncSPlvrcBWKU0yfU0YvYWWAoD\n" + + "AQgHAAAAAAAiAP9OdAPppjU1WwpqjIItkxr+VPQRT8Zm/Riw7U3F6v3OiBFHiHoF\n" + + "GBYIACwiIQUZNHvJhyRkAl+Z3z7C4AAO2YhIkuH3s+pMlACRWVabVAUCXJH05AIb\n" + + "DAAAOSQBAP4BOOIR/sGLNMOfeb5fPs/02QMieoiSjIBnijhob2U5AQC+RtOHCHx7\n" + + "TcIYl5/Uyoi+FOvPLcNw4hOv2nwUzSSVAw==\n" + + "=IiS2\n" + + "-----END PGP PRIVATE KEY BLOCK-----\n"; + + ByteArrayInputStream bIn = new ByteArrayInputStream(KEY.getBytes(StandardCharsets.UTF_8)); + ArmoredInputStream aIn = new ArmoredInputStream(bIn); + ByteArrayOutputStream bOut = new ByteArrayOutputStream(); + Streams.pipeAll(aIn, bOut); + byte[] hex = bOut.toByteArray(); + + bIn = new ByteArrayInputStream(hex); + BCPGInputStream pIn = new BCPGInputStream(bIn); + PGPObjectFactory objFac = new BcPGPObjectFactory(pIn); + + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) objFac.nextObject(); + Iterator