From 868e9c02d3a0305024d642e88b55ef4fd63c7df4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 27 Sep 2025 17:04:14 +0200 Subject: [PATCH 1/2] Replace static OpenPGPCertificate.join() methods with non-static members --- .../openpgp/api/OpenPGPCertificate.java | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java index 267cfd72ce..96ac70c078 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPCertificate.java @@ -526,25 +526,62 @@ public Date getLastModificationDateAt(Date evaluationTime) * @return merged certificate * @throws IOException if the armored data cannot be processed * @throws PGPException if a protocol level error occurs + * + * @deprecated use non-static {@link #join(String)} instead. */ + @Deprecated public static OpenPGPCertificate join(OpenPGPCertificate certificate, String armored) throws IOException, PGPException + { + return certificate.join(armored); + } + + /** + * Join two copies of the same {@link OpenPGPCertificate}, merging its {@link OpenPGPCertificateComponent components} + * into a single instance. + * + * @param certificate base certificate + * @param other copy of the same certificate, potentially carrying a different set of components + * @return merged certificate + * @throws PGPException if a protocol level error occurs + * @deprecated use non-static {@link #join(OpenPGPCertificate)} instead. + */ + @Deprecated + public static OpenPGPCertificate join(OpenPGPCertificate certificate, OpenPGPCertificate other) + throws PGPException + { + return certificate.join(other); + } + + /** + * Join two copies of the same {@link OpenPGPCertificate}, merging its {@link OpenPGPCertificateComponent components} + * into a single instance. + * The ASCII armored {@link String} might contain more than one {@link OpenPGPCertificate}. + * Items that are not a copy of the base certificate are silently ignored. + * + * @param armored ASCII armored {@link String} containing one or more copies of this certificate, + * possibly containing a different set of components + * @return merged certificate + * @throws IOException if the armored data cannot be processed + * @throws PGPException if a protocol level error occurs + */ + public OpenPGPCertificate join(String armored) + throws PGPException, IOException { ByteArrayInputStream bIn = new ByteArrayInputStream(armored.getBytes()); InputStream decoderStream = PGPUtil.getDecoderStream(bIn); BCPGInputStream wrapper = BCPGInputStream.wrap(decoderStream); - PGPObjectFactory objFac = certificate.implementation.pgpObjectFactory(wrapper); + PGPObjectFactory objFac = implementation.pgpObjectFactory(wrapper); Object next; while ((next = objFac.nextObject()) != null) { if (next instanceof PGPPublicKeyRing) { - PGPPublicKeyRing publicKeys = (PGPPublicKeyRing)next; - OpenPGPCertificate otherCert = new OpenPGPCertificate(publicKeys, certificate.implementation); + OpenPGPCertificate otherCert = new OpenPGPCertificate((PGPPublicKeyRing) next, implementation); try { - return join(certificate, otherCert); + return join(otherCert); } catch (IllegalArgumentException e) { @@ -554,7 +591,8 @@ public static OpenPGPCertificate join(OpenPGPCertificate certificate, String arm else if (next instanceof PGPSecretKeyRing) { - throw new IllegalArgumentException("Joining with a secret key is not supported."); + throw new IllegalArgumentException("Joining certificate with a secret key is not supported." + + " Try the other way round."); } else if (next instanceof PGPSignatureList) @@ -564,34 +602,33 @@ else if (next instanceof PGPSignatureList) // (self-signatures) or by a 3rd party (delegations / delegation revocations) PGPSignatureList signatures = (PGPSignatureList)next; - PGPPublicKeyRing publicKeys = certificate.getPGPPublicKeyRing(); + PGPPublicKeyRing publicKeys = getPGPPublicKeyRing(); PGPPublicKey primaryKey = publicKeys.getPublicKey(); for (Iterator it = signatures.iterator(); it.hasNext(); ) { primaryKey = PGPPublicKey.addCertification(primaryKey, it.next()); } publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, primaryKey); - return new OpenPGPCertificate(publicKeys, certificate.implementation); + return new OpenPGPCertificate(publicKeys, implementation); } } - return null; + return this; } /** * Join two copies of the same {@link OpenPGPCertificate}, merging its {@link OpenPGPCertificateComponent components} * into a single instance. * - * @param certificate base certificate - * @param other copy of the same certificate, potentially carrying a different set of components + * @param other copy of this certificate, potentially carrying a different set of components * @return merged certificate * @throws PGPException if a protocol level error occurs */ - public static OpenPGPCertificate join(OpenPGPCertificate certificate, OpenPGPCertificate other) + public OpenPGPCertificate join(OpenPGPCertificate other) throws PGPException { PGPPublicKeyRing joined = PGPPublicKeyRing.join( - certificate.getPGPPublicKeyRing(), other.getPGPPublicKeyRing()); - return new OpenPGPCertificate(joined, certificate.implementation); + getPGPPublicKeyRing(), other.getPGPPublicKeyRing()); + return new OpenPGPCertificate(joined, implementation); } /** From dadbdb182708675fb9813162401463fed7956dfd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 27 Sep 2025 17:04:40 +0200 Subject: [PATCH 2/2] Implement join() for OpenPGPKey class --- .../bouncycastle/openpgp/api/OpenPGPKey.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java index 2a0009ddda..94a5d6fe7f 100644 --- a/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java +++ b/pg/src/main/java/org/bouncycastle/openpgp/api/OpenPGPKey.java @@ -1,13 +1,17 @@ package org.bouncycastle.openpgp.api; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import org.bouncycastle.bcpg.BCPGInputStream; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.KeyIdentifier; @@ -18,9 +22,15 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyValidationException; +import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureList; +import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.api.exception.KeyPassphraseException; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptorBuilderProvider; @@ -210,6 +220,117 @@ public byte[] getEncoded(PacketFormat packetFormat) return bOut.toByteArray(); } + /** + * Return a new instance of this key with the updated signatures or subkeys from the given + * ASCII armored stream merged into. + * + * @param armored ASCII armored key, certificate or key signatures + * @return key with merged material + * @throws IOException if the key material cannot be decoded + * @throws PGPException if the key material cannot be decoded + */ + @Override + public OpenPGPKey join(String armored) + throws IOException, PGPException + { + ByteArrayInputStream bIn = new ByteArrayInputStream(armored.getBytes()); + InputStream decoderStream = PGPUtil.getDecoderStream(bIn); + BCPGInputStream wrapper = BCPGInputStream.wrap(decoderStream); + PGPObjectFactory objFac = implementation.pgpObjectFactory(wrapper); + + OpenPGPCertificate otherCert; + + Object next; + while ((next = objFac.nextObject()) != null) + { + if (next instanceof PGPPublicKeyRing) + { + PGPPublicKeyRing publicKeys = (PGPPublicKeyRing)next; + otherCert = new OpenPGPCertificate(publicKeys, implementation); + try + { + return join(otherCert); + } + catch (IllegalArgumentException e) + { + // skip over wrong certificate + } + } + + else if (next instanceof PGPSecretKeyRing) + { + PGPSecretKeyRing secretKeys = (PGPSecretKeyRing) next; + otherCert = new OpenPGPKey(secretKeys, implementation); + try + { + return join(otherCert); + } catch (IllegalArgumentException e) + { + // skip over wrong certificate + } + } + + else if (next instanceof PGPSignatureList) + { + // parse and join delegations / revocations + // those are signatures of type DIRECT_KEY or KEY_REVOCATION issued either by the primary key itself + // (self-signatures) or by a 3rd party (delegations / delegation revocations) + PGPSignatureList signatures = (PGPSignatureList)next; + + PGPPublicKeyRing publicKeys = getPGPPublicKeyRing(); + PGPPublicKey primaryKey = publicKeys.getPublicKey(); + for (Iterator it = signatures.iterator(); it.hasNext(); ) + { + primaryKey = PGPPublicKey.addCertification(primaryKey, it.next()); + } + publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, primaryKey); + PGPSecretKeyRing secretKeys = PGPSecretKeyRing.replacePublicKeys(getPGPKeyRing(), publicKeys); + return new OpenPGPKey(secretKeys, implementation); + } + } + return this; + } + + @Override + public OpenPGPKey join(OpenPGPCertificate keyOrCertificate) + throws PGPException + { + if (!getKeyIdentifier().matchesExplicit(keyOrCertificate.getKeyIdentifier())) + { + throw new IllegalArgumentException("Not the same OpenPGP key/certificate: Mismatched primary key."); + } + + PGPSecretKeyRing secretKeys = getPGPSecretKeyRing(); + if (keyOrCertificate.isSecretKey()) + { + OpenPGPKey otherKey = (OpenPGPKey) keyOrCertificate; + + // existing secret keys + List sks = new ArrayList<>(); + for (PGPSecretKey sk : getPGPSecretKeyRing()) + { + sks.add(sk); + } + + // merge new secret keys + for (PGPSecretKey sk : otherKey.getPGPSecretKeyRing()) + { + if (getPGPSecretKeyRing().getSecretKey(sk.getKeyIdentifier()) == null) + { + sks.add(sk); + } + } + + secretKeys = new PGPSecretKeyRing(sks); + } + + // merge the public parts + OpenPGPCertificate joinedCertificate = toCertificate().join(keyOrCertificate); + // join secret and public parts + secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, joinedCertificate.getPGPPublicKeyRing()); + return new OpenPGPKey(secretKeys, implementation); + } + /** * Secret key component of a {@link org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPPrimaryKey} or * {@link org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPSubkey}.