From e9dc26b1da05a349cfcdf1b754bdce084cf7f54b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 22 Oct 2021 15:42:08 +0200 Subject: [PATCH] Started working on proofs --- .../org/pgpainless/signature/ProofUtil.java | 141 ++++++++++++++++++ .../builder/AbstractSignatureBuilder.java | 15 ++ .../CertificationSignatureBuilder.java | 19 ++- .../builder/DirectKeySignatureBuilder.java | 43 ++++++ .../subpackets/BaseSignatureSubpackets.java | 2 + .../SignatureSubpacketGeneratorWrapper.java | 122 ++++++++++++++- .../signature/builder/ProofUtilTest.java | 75 ++++++++++ .../SubkeyBindingSignatureBuilderTest.java | 56 +++++++ 8 files changed, 469 insertions(+), 4 deletions(-) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/ProofUtil.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/signature/builder/DirectKeySignatureBuilder.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilderTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/ProofUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/ProofUtil.java new file mode 100644 index 00000000..81055b84 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/ProofUtil.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.builder.CertificationSignatureBuilder; +import org.pgpainless.signature.builder.DirectKeySignatureBuilder; + +public class ProofUtil { + + public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, Proof proof) + throws PGPException { + return addProofs(secretKey, protector, Collections.singletonList(proof)); + } + + public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, List proofs) + throws PGPException { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + return addProofs(secretKey, protector, info.getPrimaryUserId(), proofs); + } + + public PGPSecretKeyRing addProof(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, String userId, Proof proof) + throws PGPException { + return addProofs(secretKey, protector, userId, Collections.singletonList(proof)); + } + + public PGPSecretKeyRing addProofs(PGPSecretKeyRing secretKey, SecretKeyRingProtector protector, + @Nullable String userId, List proofs) + throws PGPException { + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPSecretKey certificationKey = secretKey.getSecretKey(); + PGPPublicKey certificationPubKey = certificationKey.getPublicKey(); + PGPSignature certification = null; + + // null userid -> make direct key sig + if (userId == null) { + PGPSignature previousCertification = info.getLatestDirectKeySelfSignature(); + if (previousCertification == null) { + throw new NoSuchElementException("No previous valid direct key signature found."); + } + + DirectKeySignatureBuilder sigBuilder = new DirectKeySignatureBuilder(certificationKey, protector, previousCertification); + for (Proof proof : proofs) { + sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); + } + certification = sigBuilder.build(certificationPubKey); + certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, certification); + } else { + if (!info.isUserIdValid(userId)) { + throw new IllegalArgumentException("User ID " + userId + " seems to not be valid for this key."); + } + PGPSignature previousCertification = info.getLatestUserIdCertification(userId); + if (previousCertification == null) { + throw new NoSuchElementException("No previous valid user-id certification found."); + } + + CertificationSignatureBuilder sigBuilder = new CertificationSignatureBuilder(certificationKey, protector, previousCertification); + for (Proof proof : proofs) { + sigBuilder.getHashedSubpackets().addNotationData(false, proof.getNotationName(), proof.getNotationValue()); + } + certification = sigBuilder.build(certificationPubKey, userId); + certificationPubKey = PGPPublicKey.addCertification(certificationPubKey, userId, certification); + } + certificationKey = PGPSecretKey.replacePublicKey(certificationKey, certificationPubKey); + secretKey = PGPSecretKeyRing.insertSecretKey(secretKey, certificationKey); + + return secretKey; + } + + public static class Proof { + public static final String NOTATION_NAME = "proof@metacode.biz"; + private final String notationValue; + + public Proof(String notationValue) { + if (notationValue == null) { + throw new IllegalArgumentException("Notation value cannot be null."); + } + String trimmed = notationValue.trim(); + if (trimmed.isEmpty()) { + throw new IllegalArgumentException("Notation value cannot be empty."); + } + this.notationValue = trimmed; + } + + public String getNotationName() { + return NOTATION_NAME; + } + + public String getNotationValue() { + return notationValue; + } + + public static Proof fromMatrixPermalink(String username, String eventPermalink) { + Pattern pattern = Pattern.compile("^https:\\/\\/matrix\\.to\\/#\\/(![a-zA-Z]{18}:matrix\\.org)\\/(\\$[a-zA-Z0-9\\-_]{43})\\?via=.*$"); + Matcher matcher = pattern.matcher(eventPermalink); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid matrix event permalink."); + } + String roomId = matcher.group(1); + String eventId = matcher.group(2); + return new Proof(String.format("matrix:u/%s?org.keyoxide.r=%s&org.keyoxide.e=%s", username, roomId, eventId)); + } + + @Override + public String toString() { + return getNotationName() + "=" + getNotationValue(); + } + } + + public static List getProofs(PGPSignature signature) { + PGPSignatureSubpacketVector hashedSubpackets = signature.getHashedSubPackets(); + NotationData[] notations = hashedSubpackets.getNotationDataOccurrences(); + + List proofs = new ArrayList<>(); + for (NotationData notation : notations) { + if (notation.getNotationName().equals(Proof.NOTATION_NAME)) { + proofs.add(new Proof(notation.getNotationValue())); + } + } + return proofs; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java index 7f1425d4..3c859883 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java @@ -10,6 +10,7 @@ import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.pgpainless.PGPainless; import org.pgpainless.algorithm.HashAlgorithm; @@ -46,6 +47,20 @@ public abstract class AbstractSignatureBuilder hashAlgorithmPreferences = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey); return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/CertificationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/CertificationSignatureBuilder.java index 792658ee..f094c4fc 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/CertificationSignatureBuilder.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/CertificationSignatureBuilder.java @@ -4,6 +4,8 @@ package org.pgpainless.signature.builder; +import javax.annotation.Nonnull; + import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; @@ -12,6 +14,7 @@ import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; import org.pgpainless.algorithm.SignatureType; import org.pgpainless.exception.WrongPassphraseException; import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; public class CertificationSignatureBuilder extends AbstractSignatureBuilder { @@ -19,6 +22,18 @@ public class CertificationSignatureBuilder extends AbstractSignatureBuilder +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.subpackets.SelfSignatureSubpackets; + +public class DirectKeySignatureBuilder extends AbstractSignatureBuilder { + + public DirectKeySignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector, PGPSignature archetypeSignature) throws WrongPassphraseException { + super(certificationKey, protector, archetypeSignature); + } + + public DirectKeySignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) throws WrongPassphraseException { + super(signatureType, signingKey, protector); + } + + public SelfSignatureSubpackets getHashedSubpackets() { + return hashedSubpackets; + } + + public SelfSignatureSubpackets getUnhashedSubpackets() { + return unhashedSubpackets; + } + + public PGPSignature build(PGPPublicKey key) throws PGPException { + return buildAndInitSignatureGenerator() + .generateCertification(key); + } + + @Override + protected boolean isValidSignatureType(SignatureType type) { + return type == SignatureType.DIRECT_KEY; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java index 6980d131..3b824f11 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -64,6 +64,8 @@ public interface BaseSignatureSubpackets { SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue); + SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue); + SignatureSubpacketGeneratorWrapper addNotationData(@Nonnull NotationData notationData); SignatureSubpacketGeneratorWrapper clearNotationData(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapper.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapper.java index 5a4e72d3..6bb9aaf4 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapper.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapper.java @@ -39,6 +39,7 @@ import org.bouncycastle.bcpg.sig.TrustSignature; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.Feature; import org.pgpainless.algorithm.HashAlgorithm; @@ -71,16 +72,125 @@ public class SignatureSubpacketGeneratorWrapper private PrimaryUserID primaryUserId; private Revocable revocable; private RevocationReason revocationReason; + private final List unsupportedSubpackets = new ArrayList<>(); public SignatureSubpacketGeneratorWrapper() { setSignatureCreationTime(new Date()); } public SignatureSubpacketGeneratorWrapper(PGPPublicKey issuer) { - this(); + setSignatureCreationTime(new Date()); setIssuerFingerprintAndKeyId(issuer); } + public SignatureSubpacketGeneratorWrapper(PGPPublicKey issuer, PGPSignatureSubpacketVector base) { + extractSubpacketsFromVector(base); + setSignatureCreationTime(new Date()); + setIssuerFingerprintAndKeyId(issuer); + } + + public SignatureSubpacketGeneratorWrapper(PGPSignatureSubpacketVector base) { + extractSubpacketsFromVector(base); + setSignatureCreationTime(new Date()); + } + + private void extractSubpacketsFromVector(PGPSignatureSubpacketVector base) { + for (SignatureSubpacket subpacket : base.toArray()) { + org.pgpainless.algorithm.SignatureSubpacket type = org.pgpainless.algorithm.SignatureSubpacket.fromCode(subpacket.getType()); + switch (type) { + case signatureCreationTime: + case issuerKeyId: + case issuerFingerprint: + // ignore, we override this anyways + break; + case signatureExpirationTime: + SignatureExpirationTime sigExpTime = (SignatureExpirationTime) subpacket; + setSignatureExpirationTime(sigExpTime.isCritical(), sigExpTime.getTime()); + break; + case exportableCertification: + Exportable exp = (Exportable) subpacket; + setExportable(exp.isCritical(), exp.isExportable()); + break; + case trustSignature: + TrustSignature trustSignature = (TrustSignature) subpacket; + setTrust(trustSignature.isCritical(), trustSignature.getDepth(), trustSignature.getTrustAmount()); + break; + case revocable: + Revocable rev = (Revocable) subpacket; + setRevocable(rev.isCritical(), rev.isRevocable()); + break; + case keyExpirationTime: + KeyExpirationTime keyExpTime = (KeyExpirationTime) subpacket; + setKeyExpirationTime(keyExpTime.isCritical(), keyExpTime.getTime()); + break; + case preferredSymmetricAlgorithms: + setPreferredSymmetricKeyAlgorithms((PreferredAlgorithms) subpacket); + break; + case revocationKey: + RevocationKey revocationKey = (RevocationKey) subpacket; + addRevocationKey(revocationKey); + break; + case notationData: + NotationData notationData = (NotationData) subpacket; + addNotationData(notationData.isCritical(), notationData.getNotationName(), notationData.getNotationValue()); + break; + case preferredHashAlgorithms: + setPreferredHashAlgorithms((PreferredAlgorithms) subpacket); + break; + case preferredCompressionAlgorithms: + setPreferredCompressionAlgorithms((PreferredAlgorithms) subpacket); + break; + case primaryUserId: + PrimaryUserID primaryUserID = (PrimaryUserID) subpacket; + setPrimaryUserId(primaryUserID); + break; + case keyFlags: + KeyFlags flags = (KeyFlags) subpacket; + setKeyFlags(flags.isCritical(), KeyFlag.fromBitmask(flags.getFlags()).toArray(new KeyFlag[0])); + break; + case signerUserId: + SignerUserID signerUserID = (SignerUserID) subpacket; + setSignerUserId(signerUserID.isCritical(), signerUserID.getID()); + break; + case revocationReason: + RevocationReason reason = (RevocationReason) subpacket; + setRevocationReason(reason.isCritical(), + RevocationAttributes.Reason.fromCode(reason.getRevocationReason()), + reason.getRevocationDescription()); + break; + case features: + Features f = (Features) subpacket; + setFeatures(f.isCritical(), Feature.fromBitmask(f.getData()[0]).toArray(new Feature[0])); + break; + case signatureTarget: + SignatureTarget target = (SignatureTarget) subpacket; + setSignatureTarget(target.isCritical(), + PublicKeyAlgorithm.fromId(target.getPublicKeyAlgorithm()), + HashAlgorithm.fromId(target.getHashAlgorithm()), + target.getHashData()); + break; + case embeddedSignature: + EmbeddedSignature embeddedSignature = (EmbeddedSignature) subpacket; + addEmbeddedSignature(embeddedSignature); + break; + case intendedRecipientFingerprint: + IntendedRecipientFingerprint intendedRecipientFingerprint = (IntendedRecipientFingerprint) subpacket; + addIntendedRecipientFingerprint(intendedRecipientFingerprint); + break; + + case regularExpression: + case keyServerPreferences: + case preferredKeyServers: + case policyUrl: + case placeholder: + case preferredAEADAlgorithms: + case attestedCertification: + unsupportedSubpackets.add(subpacket); + break; + } + } + } + public PGPSignatureSubpacketGenerator getGenerator() { PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); @@ -113,6 +223,9 @@ public class SignatureSubpacketGeneratorWrapper addSubpacket(generator, primaryUserId); addSubpacket(generator, revocable); addSubpacket(generator, revocationReason); + for (SignatureSubpacket subpacket : unsupportedSubpackets) { + addSubpacket(generator, subpacket); + } return generator; } @@ -381,7 +494,12 @@ public class SignatureSubpacketGeneratorWrapper @Override public SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue) { - return addNotationData(new NotationData(isCritical, true, notationName, notationValue)); + return addNotationData(isCritical, true, notationName, notationValue); + } + + @Override + public SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, boolean isHumanReadable, @Nonnull String notationName, @Nonnull String notationValue) { + return addNotationData(new NotationData(isCritical, isHumanReadable, notationName, notationValue)); } @Override diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java new file mode 100644 index 00000000..a040ee93 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/ProofUtilTest.java @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.signature.ProofUtil; + +public class ProofUtilTest { + + @Test + public void testEmptyProofThrows() { + assertThrows(IllegalArgumentException.class, () -> new ProofUtil.Proof("")); + } + + @Test + public void testNullProofThrows() { + assertThrows(IllegalArgumentException.class, () -> new ProofUtil.Proof(null)); + } + + @Test + public void proofIsTrimmed() { + ProofUtil.Proof proof = new ProofUtil.Proof(" foo:bar "); + assertEquals("proof@metacode.biz=foo:bar", proof.toString()); + } + + @Test + public void testMatrixProof() { + String matrixUser = "@foo:matrix.org"; + String permalink = "https://matrix.to/#/!dBfQZxCoGVmSTujfiv:matrix.org/$3dVX1nv3lmwnKxc0mgto_Sf-REVr45Z6G7LWLWal10w?via=chat.matrix.org"; + ProofUtil.Proof proof = ProofUtil.Proof.fromMatrixPermalink(matrixUser, permalink); + + assertEquals("proof@metacode.biz=matrix:u/@foo:matrix.org?org.keyoxide.r=!dBfQZxCoGVmSTujfiv:matrix.org&org.keyoxide.e=$3dVX1nv3lmwnKxc0mgto_Sf-REVr45Z6G7LWLWal10w", + proof.toString()); + } + + @Test + public void testXmppBasicProof() { + String jid = "alice@pgpainless.org"; + ProofUtil.Proof proof = new ProofUtil.Proof("xmpp:" + jid); + + assertEquals("proof@metacode.biz=xmpp:alice@pgpainless.org", proof.toString()); + } + + @Test + public void testAddProof() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, InterruptedException { + String userId = "Alice "; + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .modernKeyRing(userId, null); + Thread.sleep(1000L); + secretKey = new ProofUtil() + .addProof(secretKey, SecretKeyRingProtector.unprotectedKeys(), new ProofUtil.Proof("xmpp:alice@pgpainless.org")); + + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + PGPSignature signature = info.getLatestUserIdCertification(userId); + assertNotNull(signature); + assertFalse(ProofUtil.getProofs(signature).isEmpty()); + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilderTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilderTest.java new file mode 100644 index 00000000..341940e5 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilderTest.java @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.EncryptionPurpose; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.key.info.KeyRingInfo; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.util.Passphrase; + +public class SubkeyBindingSignatureBuilderTest { + + @Test + public void testBindSubkeyWithCustomNotation() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + PGPSecretKeyRing secretKey = PGPainless.generateKeyRing() + .modernKeyRing("Alice ", "passphrase"); + KeyRingInfo info = PGPainless.inspectKeyRing(secretKey); + List previousSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + SecretKeyRingProtector protector = SecretKeyRingProtector.unlockAllKeysWith(Passphrase.fromPassword("passphrase"), secretKey); + + PGPSecretKeyRing tempSubkeyRing = PGPainless.generateKeyRing() + .modernKeyRing("Subkeys", null); + PGPPublicKey subkey = PGPainless.inspectKeyRing(tempSubkeyRing) + .getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS).get(0); + + SubkeyBindingSignatureBuilder skbb = new SubkeyBindingSignatureBuilder(SignatureType.SUBKEY_BINDING, secretKey.getSecretKey(), protector); + skbb.getHashedSubpackets().addNotationData(false, "testnotation@pgpainless.org", "hello-world"); + skbb.getHashedSubpackets().setKeyFlags(KeyFlag.ENCRYPT_COMMS, KeyFlag.ENCRYPT_STORAGE); + PGPSignature binding = skbb.build(subkey); + subkey = PGPPublicKey.addCertification(subkey, binding); + PGPSecretKey secSubkey = tempSubkeyRing.getSecretKey(subkey.getKeyID()); + secSubkey = PGPSecretKey.replacePublicKey(secSubkey, subkey); + secretKey = PGPSecretKeyRing.insertSecretKey(secretKey, secSubkey); + + info = PGPainless.inspectKeyRing(secretKey); + List nextSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS); + assertEquals(previousSubkeys.size() + 1, nextSubkeys.size()); + } +}