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 new file mode 100644 index 00000000..7f1425d4 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/AbstractSignatureBuilder.java @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +import java.util.Set; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKey; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.negotiation.HashAlgorithmNegotiator; +import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.OpenPgpKeyAttributeUtil; +import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorWrapper; + +public abstract class AbstractSignatureBuilder> { + protected final PGPPrivateKey privateSigningKey; + protected final PGPPublicKey publicSigningKey; + + protected HashAlgorithm hashAlgorithm; + protected SignatureType signatureType; + + protected SignatureSubpacketGeneratorWrapper unhashedSubpackets; + protected SignatureSubpacketGeneratorWrapper hashedSubpackets; + + public AbstractSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) + throws WrongPassphraseException { + if (!isValidSignatureType(signatureType)) { + throw new IllegalArgumentException("Invalid signature type."); + } + this.signatureType = signatureType; + this.privateSigningKey = UnlockSecretKey.unlockSecretKey(signingKey, protector); + this.publicSigningKey = signingKey.getPublicKey(); + this.hashAlgorithm = negotiateHashAlgorithm(publicSigningKey); + + unhashedSubpackets = new SignatureSubpacketGeneratorWrapper(); + hashedSubpackets = new SignatureSubpacketGeneratorWrapper(publicSigningKey); + } + + protected HashAlgorithm negotiateHashAlgorithm(PGPPublicKey publicKey) { + Set hashAlgorithmPreferences = OpenPgpKeyAttributeUtil.getOrGuessPreferredHashAlgorithms(publicKey); + return HashAlgorithmNegotiator.negotiateSignatureHashAlgorithm(PGPainless.getPolicy()) + .negotiateHashAlgorithm(hashAlgorithmPreferences); + } + + public B setSignatureType(SignatureType type) { + if (!isValidSignatureType(type)) { + throw new IllegalArgumentException("Invalid signature type: " + type); + } + this.signatureType = type; + return (B) this; + } + + protected PGPSignatureGenerator buildAndInitSignatureGenerator() throws PGPException { + PGPSignatureGenerator generator = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder( + publicSigningKey.getAlgorithm(), hashAlgorithm.getAlgorithmId() + ) + ); + generator.setUnhashedSubpackets(unhashedSubpackets.getGenerator().generate()); + generator.setHashedSubpackets(hashedSubpackets.getGenerator().generate()); + generator.init(signatureType.getCode(), privateSigningKey); + return generator; + } + + protected abstract boolean isValidSignatureType(SignatureType type); +} 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 new file mode 100644 index 00000000..03801204 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/CertificationSignatureBuilder.java @@ -0,0 +1,38 @@ +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.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.exception.WrongPassphraseException; +import org.pgpainless.key.protection.SecretKeyRingProtector; + +public class CertificationSignatureBuilder extends AbstractSignatureBuilder { + + public CertificationSignatureBuilder(PGPSecretKey certificationKey, SecretKeyRingProtector protector) throws WrongPassphraseException { + super(SignatureType.GENERIC_CERTIFICATION, certificationKey, protector); + } + + public PGPSignature build(PGPPublicKey certifiedKey, String userId) throws PGPException { + return buildAndInitSignatureGenerator().generateCertification(userId, certifiedKey); + } + + public PGPSignature build(PGPPublicKey certifiedKey, PGPUserAttributeSubpacketVector userAttribute) throws PGPException { + return buildAndInitSignatureGenerator().generateCertification(userAttribute, certifiedKey); + } + + @Override + protected boolean isValidSignatureType(SignatureType type) { + switch (signatureType) { + case GENERIC_CERTIFICATION: + case NO_CERTIFICATION: + case CASUAL_CERTIFICATION: + case POSITIVE_CERTIFICATION: + return true; + default: + return false; + } + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java new file mode 100644 index 00000000..bd7bef31 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/RevocationSignatureBuilder.java @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.builder; + +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.RevocationSignatureSubpackets; + +public class RevocationSignatureBuilder extends AbstractSignatureBuilder { + + public RevocationSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) throws WrongPassphraseException { + super(signatureType, signingKey, protector); + } + + @Override + protected boolean isValidSignatureType(SignatureType type) { + switch (type) { + case KEY_REVOCATION: + case SUBKEY_REVOCATION: + case CERTIFICATION_REVOCATION: + return true; + default: + return false; + } + } + + public RevocationSignatureSubpackets getHashedSubpackets() { + return hashedSubpackets; + } + + public RevocationSignatureSubpackets getUnhashedSubpackets() { + return unhashedSubpackets; + } + + public PGPSignature build() +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java new file mode 100644 index 00000000..8e74510b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/builder/SubkeyBindingSignatureBuilder.java @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// 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 SubkeyBindingSignatureBuilder extends AbstractSignatureBuilder { + + public SubkeyBindingSignatureBuilder(SignatureType signatureType, PGPSecretKey signingKey, SecretKeyRingProtector protector) throws WrongPassphraseException { + super(signatureType, signingKey, protector); + } + + @Override + protected boolean isValidSignatureType(SignatureType type) { + return type == SignatureType.SUBKEY_BINDING; + } + + public SelfSignatureSubpackets getHashedSubpackets() { + return hashedSubpackets; + } + + public SelfSignatureSubpackets getUnhashedSubpackets() { + return unhashedSubpackets; + } + + public PGPSignature build(PGPPublicKey subkey) throws PGPException { + return buildAndInitSignatureGenerator() + .generateCertification(publicSigningKey, subkey); + } +} 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 new file mode 100644 index 00000000..6980d131 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/BaseSignatureSubpackets.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import java.io.IOException; +import java.util.Date; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.sig.EmbeddedSignature; +import org.bouncycastle.bcpg.sig.Exportable; +import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.bcpg.sig.IssuerKeyID; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.Revocable; +import org.bouncycastle.bcpg.sig.SignatureCreationTime; +import org.bouncycastle.bcpg.sig.SignatureExpirationTime; +import org.bouncycastle.bcpg.sig.SignatureTarget; +import org.bouncycastle.bcpg.sig.SignerUserID; +import org.bouncycastle.bcpg.sig.TrustSignature; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.PublicKeyAlgorithm; + +public interface BaseSignatureSubpackets { + + SignatureSubpacketGeneratorWrapper setIssuerFingerprintAndKeyId(PGPPublicKey key); + + SignatureSubpacketGeneratorWrapper setIssuerKeyId(long keyId); + + SignatureSubpacketGeneratorWrapper setIssuerKeyId(boolean isCritical, long keyId); + + SignatureSubpacketGeneratorWrapper setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID); + + SignatureSubpacketGeneratorWrapper setIssuerFingerprint(@Nonnull PGPPublicKey key); + + SignatureSubpacketGeneratorWrapper setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key); + + SignatureSubpacketGeneratorWrapper setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint); + + SignatureSubpacketGeneratorWrapper setSignatureCreationTime(@Nonnull Date creationTime); + + SignatureSubpacketGeneratorWrapper setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime); + + SignatureSubpacketGeneratorWrapper setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime); + + SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime); + + SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime); + + SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(boolean isCritical, long seconds); + + SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime); + + SignatureSubpacketGeneratorWrapper setSignerUserId(@Nonnull String userId); + + SignatureSubpacketGeneratorWrapper setSignerUserId(boolean isCritical, @Nonnull String userId); + + SignatureSubpacketGeneratorWrapper setSignerUserId(@Nullable SignerUserID signerUserId); + + SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue); + + SignatureSubpacketGeneratorWrapper addNotationData(@Nonnull NotationData notationData); + + SignatureSubpacketGeneratorWrapper clearNotationData(); + + SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient); + + SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient); + + SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint); + + SignatureSubpacketGeneratorWrapper clearIntendedRecipientFingerprints(); + + SignatureSubpacketGeneratorWrapper setExportable(boolean isCritical, boolean isExportable); + + SignatureSubpacketGeneratorWrapper setExportable(@Nullable Exportable exportable); + + SignatureSubpacketGeneratorWrapper setRevocable(boolean isCritical, boolean isRevocable); + + SignatureSubpacketGeneratorWrapper setRevocable(@Nullable Revocable revocable); + + SignatureSubpacketGeneratorWrapper setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); + + SignatureSubpacketGeneratorWrapper setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData); + + SignatureSubpacketGeneratorWrapper setSignatureTarget(@Nullable SignatureTarget signatureTarget); + + SignatureSubpacketGeneratorWrapper setTrust(int depth, int amount); + + SignatureSubpacketGeneratorWrapper setTrust(boolean isCritical, int depth, int amount); + + SignatureSubpacketGeneratorWrapper setTrust(@Nullable TrustSignature trust); + + SignatureSubpacketGeneratorWrapper addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException; + + SignatureSubpacketGeneratorWrapper addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException; + + SignatureSubpacketGeneratorWrapper addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature); + + SignatureSubpacketGeneratorWrapper clearEmbeddedSignatures(); +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java new file mode 100644 index 00000000..b71c3a05 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/RevocationSignatureSubpackets.java @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.sig.RevocationReason; +import org.pgpainless.key.util.RevocationAttributes; + +public interface RevocationSignatureSubpackets extends BaseSignatureSubpackets { + + SignatureSubpacketGeneratorWrapper setRevocationReason(RevocationAttributes revocationAttributes); + + SignatureSubpacketGeneratorWrapper setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes); + + SignatureSubpacketGeneratorWrapper setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description); + + SignatureSubpacketGeneratorWrapper setRevocationReason(@Nullable RevocationReason reason); +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketCallback.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketCallback.java new file mode 100644 index 00000000..a0d5f4c3 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpacketCallback.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +public interface SelfSignatureSubpacketCallback { + + void modifyHashedSubpackets(SelfSignatureSubpackets subpackets); + + void modifyUnhashedSubpackets(SelfSignatureSubpackets subpackets); + +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java new file mode 100644 index 00000000..6643902b --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SelfSignatureSubpackets.java @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import java.util.Date; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.KeyExpirationTime; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.bcpg.sig.RevocationKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; + +public interface SelfSignatureSubpackets extends BaseSignatureSubpackets { + + SignatureSubpacketGeneratorWrapper setKeyFlags(KeyFlag... keyFlags); + + SignatureSubpacketGeneratorWrapper setKeyFlags(boolean isCritical, KeyFlag... keyFlags); + + SignatureSubpacketGeneratorWrapper setKeyFlags(@Nullable KeyFlags keyFlags); + + SignatureSubpacketGeneratorWrapper setPrimaryUserId(); + + SignatureSubpacketGeneratorWrapper setPrimaryUserId(boolean isCritical); + + SignatureSubpacketGeneratorWrapper setPrimaryUserId(@Nullable PrimaryUserID primaryUserId); + + SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime); + + SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); + + SignatureSubpacketGeneratorWrapper setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime); + + SignatureSubpacketGeneratorWrapper setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration); + + SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime); + + SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(HashAlgorithm... algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(boolean isCritical, Set algorithms); + + SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms); + + SignatureSubpacketGeneratorWrapper addRevocationKey(@Nonnull PGPPublicKey revocationKey); + + SignatureSubpacketGeneratorWrapper addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey); + + SignatureSubpacketGeneratorWrapper addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey); + + SignatureSubpacketGeneratorWrapper addRevocationKey(@Nonnull RevocationKey revocationKey); + + SignatureSubpacketGeneratorWrapper clearRevocationKeys(); + + SignatureSubpacketGeneratorWrapper setFeatures(Feature... features); + + SignatureSubpacketGeneratorWrapper setFeatures(boolean isCritical, Feature... features); + + SignatureSubpacketGeneratorWrapper setFeatures(@Nullable Features features); +} 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 new file mode 100644 index 00000000..59ce6a47 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapper.java @@ -0,0 +1,583 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.EmbeddedSignature; +import org.bouncycastle.bcpg.sig.Exportable; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.bcpg.sig.IssuerKeyID; +import org.bouncycastle.bcpg.sig.KeyExpirationTime; +import org.bouncycastle.bcpg.sig.KeyFlags; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.bcpg.sig.PrimaryUserID; +import org.bouncycastle.bcpg.sig.Revocable; +import org.bouncycastle.bcpg.sig.RevocationKey; +import org.bouncycastle.bcpg.sig.RevocationReason; +import org.bouncycastle.bcpg.sig.SignatureCreationTime; +import org.bouncycastle.bcpg.sig.SignatureExpirationTime; +import org.bouncycastle.bcpg.sig.SignatureTarget; +import org.bouncycastle.bcpg.sig.SignerUserID; +import org.bouncycastle.bcpg.sig.TrustSignature; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.key.util.RevocationAttributes; + +public class SignatureSubpacketGeneratorWrapper + implements BaseSignatureSubpackets, SelfSignatureSubpackets, RevocationSignatureSubpackets { + + private SignatureCreationTime signatureCreationTime; + private SignatureExpirationTime signatureExpirationTime; + private IssuerKeyID issuerKeyID; + private IssuerFingerprint issuerFingerprint; + private final List notationDataList = new ArrayList<>(); + private final List intendedRecipientFingerprintList = new ArrayList<>(); + private final List revocationKeyList = new ArrayList<>(); + private Exportable exportable; + private SignatureTarget signatureTarget; + private Features features; + private KeyFlags keyFlags; + private TrustSignature trust; + private PreferredAlgorithms preferredCompressionAlgorithms; + private PreferredAlgorithms preferredSymmetricKeyAlgorithms; + private PreferredAlgorithms preferredHashAlgorithms; + private final List embeddedSignatureList = new ArrayList<>(); + private SignerUserID signerUserId; + private KeyExpirationTime keyExpirationTime; + private PrimaryUserID primaryUserId; + private Revocable revocable; + private RevocationReason revocationReason; + + public SignatureSubpacketGeneratorWrapper() { + setSignatureCreationTime(new Date()); + } + + public SignatureSubpacketGeneratorWrapper(PGPPublicKey issuer) { + this(); + setIssuerFingerprintAndKeyId(issuer); + } + + public PGPSignatureSubpacketGenerator getGenerator() { + PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator(); + + addSubpacket(generator, issuerKeyID); + addSubpacket(generator, issuerFingerprint); + addSubpacket(generator, signatureCreationTime); + addSubpacket(generator, signatureExpirationTime); + addSubpacket(generator, exportable); + for (NotationData notationData : notationDataList) { + addSubpacket(generator, notationData); + } + for (IntendedRecipientFingerprint intendedRecipientFingerprint : intendedRecipientFingerprintList) { + addSubpacket(generator, intendedRecipientFingerprint); + } + for (RevocationKey revocationKey : revocationKeyList) { + addSubpacket(generator, revocationKey); + } + addSubpacket(generator, signatureTarget); + addSubpacket(generator, features); + addSubpacket(generator, keyFlags); + addSubpacket(generator, trust); + addSubpacket(generator, preferredCompressionAlgorithms); + addSubpacket(generator, preferredSymmetricKeyAlgorithms); + addSubpacket(generator, preferredHashAlgorithms); + for (EmbeddedSignature embeddedSignature : embeddedSignatureList) { + addSubpacket(generator, embeddedSignature); + } + addSubpacket(generator, signerUserId); + addSubpacket(generator, keyExpirationTime); + addSubpacket(generator, primaryUserId); + addSubpacket(generator, revocable); + addSubpacket(generator, revocationReason); + + return generator; + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerFingerprintAndKeyId(PGPPublicKey key) { + setIssuerKeyId(key.getKeyID()); + setIssuerFingerprint(key); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerKeyId(long keyId) { + return setIssuerKeyId(true, keyId); + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerKeyId(boolean isCritical, long keyId) { + return setIssuerKeyId(new IssuerKeyID(isCritical, keyId)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerKeyId(@Nullable IssuerKeyID issuerKeyID) { + this.issuerKeyID = issuerKeyID; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerFingerprint(@Nonnull PGPPublicKey key) { + return setIssuerFingerprint(true, key); + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerFingerprint(boolean isCritical, @Nonnull PGPPublicKey key) { + return setIssuerFingerprint(new IssuerFingerprint(isCritical, key.getVersion(), key.getFingerprint())); + } + + @Override + public SignatureSubpacketGeneratorWrapper setIssuerFingerprint(@Nullable IssuerFingerprint fingerprint) { + this.issuerFingerprint = fingerprint; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyFlags(KeyFlag... keyFlags) { + return setKeyFlags(true, keyFlags); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyFlags(boolean isCritical, KeyFlag... keyFlags) { + int bitmask = KeyFlag.toBitmask(keyFlags); + return setKeyFlags(new KeyFlags(isCritical, bitmask)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyFlags(@Nullable KeyFlags keyFlags) { + this.keyFlags = keyFlags; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureCreationTime(@Nonnull Date creationTime) { + return setSignatureCreationTime(true, creationTime); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureCreationTime(boolean isCritical, @Nonnull Date creationTime) { + return setSignatureCreationTime(new SignatureCreationTime(isCritical, creationTime)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureCreationTime(@Nullable SignatureCreationTime signatureCreationTime) { + this.signatureCreationTime = signatureCreationTime; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(@Nonnull Date creationTime, @Nonnull Date expirationTime) { + return setSignatureExpirationTime(true, creationTime, expirationTime); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(boolean isCritical, @Nonnull Date creationTime, @Nonnull Date expirationTime) { + return setSignatureExpirationTime(isCritical, (expirationTime.getTime() / 1000) - (creationTime.getTime() / 1000)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(boolean isCritical, long seconds) { + if (seconds < 0) { + throw new IllegalArgumentException("Expiration time cannot be negative."); + } + return setSignatureExpirationTime(new SignatureExpirationTime(isCritical, seconds)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureExpirationTime(@Nullable SignatureExpirationTime expirationTime) { + this.signatureExpirationTime = expirationTime; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignerUserId(@Nonnull String userId) { + return setSignerUserId(false, userId); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignerUserId(boolean isCritical, @Nonnull String userId) { + return setSignerUserId(new SignerUserID(isCritical, userId)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignerUserId(@Nullable SignerUserID signerUserId) { + this.signerUserId = signerUserId; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setPrimaryUserId() { + return setPrimaryUserId(true); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPrimaryUserId(boolean isCritical) { + return setPrimaryUserId(new PrimaryUserID(isCritical, true)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPrimaryUserId(@Nullable PrimaryUserID primaryUserId) { + this.primaryUserId = primaryUserId; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nonnull PGPPublicKey key, @Nonnull Date keyExpirationTime) { + return setKeyExpirationTime(key.getCreationTime(), keyExpirationTime); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { + return setKeyExpirationTime(true, keyCreationTime, keyExpirationTime); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyExpirationTime(boolean isCritical, @Nonnull Date keyCreationTime, @Nonnull Date keyExpirationTime) { + return setKeyExpirationTime(isCritical, (keyExpirationTime.getTime() / 1000) - (keyCreationTime.getTime() / 1000)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyExpirationTime(boolean isCritical, long secondsFromCreationToExpiration) { + if (secondsFromCreationToExpiration < 0) { + throw new IllegalArgumentException("Seconds from key creation to expiration cannot be less than 0."); + } + return setKeyExpirationTime(new KeyExpirationTime(isCritical, secondsFromCreationToExpiration)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setKeyExpirationTime(@Nullable KeyExpirationTime keyExpirationTime) { + this.keyExpirationTime = keyExpirationTime; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(CompressionAlgorithm... algorithms) { + return setPreferredCompressionAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(Set algorithms) { + return setPreferredCompressionAlgorithms(true, algorithms); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(boolean isCritical, Set algorithms) { + int[] ids = new int[algorithms.size()]; + Iterator iterator = algorithms.iterator(); + for (int i = 0; i < algorithms.size(); i++) { + ids[i] = iterator.next().getAlgorithmId(); + } + return setPreferredCompressionAlgorithms(new PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_COMP_ALGS, isCritical, ids)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredCompressionAlgorithms(@Nullable PreferredAlgorithms algorithms) { + if (algorithms == null) { + this.preferredCompressionAlgorithms = null; + return this; + } + + if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_COMP_ALGS) { + throw new IllegalArgumentException("Invalid preferred compression algorithms type."); + } + this.preferredCompressionAlgorithms = algorithms; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm... algorithms) { + return setPreferredSymmetricKeyAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(Set algorithms) { + return setPreferredSymmetricKeyAlgorithms(true, algorithms); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(boolean isCritical, Set algorithms) { + int[] ids = new int[algorithms.size()]; + Iterator iterator = algorithms.iterator(); + for (int i = 0; i < algorithms.size(); i++) { + ids[i] = iterator.next().getAlgorithmId(); + } + return setPreferredSymmetricKeyAlgorithms(new PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_SYM_ALGS, isCritical, ids)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredSymmetricKeyAlgorithms(@Nullable PreferredAlgorithms algorithms) { + if (algorithms == null) { + this.preferredSymmetricKeyAlgorithms = null; + return this; + } + + if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_SYM_ALGS) { + throw new IllegalArgumentException("Invalid preferred symmetric key algorithms type."); + } + this.preferredSymmetricKeyAlgorithms = algorithms; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(HashAlgorithm... algorithms) { + return setPreferredHashAlgorithms(new LinkedHashSet<>(Arrays.asList(algorithms))); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(Set algorithms) { + return setPreferredHashAlgorithms(true, algorithms); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(boolean isCritical, Set algorithms) { + int[] ids = new int[algorithms.size()]; + Iterator iterator = algorithms.iterator(); + for (int i = 0; i < ids.length; i++) { + ids[i] = iterator.next().getAlgorithmId(); + } + return setPreferredHashAlgorithms(new PreferredAlgorithms( + SignatureSubpacketTags.PREFERRED_HASH_ALGS, isCritical, ids)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setPreferredHashAlgorithms(@Nullable PreferredAlgorithms algorithms) { + if (algorithms == null) { + preferredHashAlgorithms = null; + return this; + } + + if (algorithms.getType() != SignatureSubpacketTags.PREFERRED_HASH_ALGS) { + throw new IllegalArgumentException("Invalid preferred hash algorithms type."); + } + this.preferredHashAlgorithms = algorithms; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper addNotationData(boolean isCritical, @Nonnull String notationName, @Nonnull String notationValue) { + return addNotationData(new NotationData(isCritical, true, notationName, notationValue)); + } + + @Override + public SignatureSubpacketGeneratorWrapper addNotationData(@Nonnull NotationData notationData) { + notationDataList.add(notationData); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper clearNotationData() { + notationDataList.clear(); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(@Nonnull PGPPublicKey recipient) { + return addIntendedRecipientFingerprint(false, recipient); + } + + @Override + public SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(boolean isCritical, @Nonnull PGPPublicKey recipient) { + return addIntendedRecipientFingerprint(new IntendedRecipientFingerprint(isCritical, recipient.getVersion(), recipient.getFingerprint())); + } + + @Override + public SignatureSubpacketGeneratorWrapper addIntendedRecipientFingerprint(IntendedRecipientFingerprint intendedRecipientFingerprint) { + this.intendedRecipientFingerprintList.add(intendedRecipientFingerprint); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper clearIntendedRecipientFingerprints() { + intendedRecipientFingerprintList.clear(); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setExportable(boolean isCritical, boolean isExportable) { + return setExportable(new Exportable(isCritical, isExportable)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setExportable(@Nullable Exportable exportable) { + this.exportable = exportable; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocable(boolean isCritical, boolean isRevocable) { + return setRevocable(new Revocable(isCritical, isRevocable)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocable(@Nullable Revocable revocable) { + this.revocable = revocable; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper addRevocationKey(@Nonnull PGPPublicKey revocationKey) { + return addRevocationKey(true, revocationKey); + } + + @Override + public SignatureSubpacketGeneratorWrapper addRevocationKey(boolean isCritical, @Nonnull PGPPublicKey revocationKey) { + return addRevocationKey(isCritical, false, revocationKey); + } + + @Override + public SignatureSubpacketGeneratorWrapper addRevocationKey(boolean isCritical, boolean isSensitive, @Nonnull PGPPublicKey revocationKey) { + byte clazz = (byte) 0x80; + clazz |= (isSensitive ? 0x40 : 0x00); + return addRevocationKey(new RevocationKey(isCritical, clazz, revocationKey.getAlgorithm(), revocationKey.getFingerprint())); + } + + @Override + public SignatureSubpacketGeneratorWrapper addRevocationKey(@Nonnull RevocationKey revocationKey) { + this.revocationKeyList.add(revocationKey); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper clearRevocationKeys() { + revocationKeyList.clear(); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocationReason(RevocationAttributes revocationAttributes) { + return setRevocationReason(true, revocationAttributes); + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocationReason(boolean isCritical, RevocationAttributes revocationAttributes) { + return setRevocationReason(isCritical, revocationAttributes.getReason(), revocationAttributes.getDescription()); + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocationReason(boolean isCritical, RevocationAttributes.Reason reason, @Nonnull String description) { + return setRevocationReason(new RevocationReason(isCritical, reason.code(), description)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setRevocationReason(@Nullable RevocationReason reason) { + this.revocationReason = reason; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureTarget(@Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { + return setSignatureTarget(true, keyAlgorithm, hashAlgorithm, hashData); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureTarget(boolean isCritical, @Nonnull PublicKeyAlgorithm keyAlgorithm, @Nonnull HashAlgorithm hashAlgorithm, @Nonnull byte[] hashData) { + return setSignatureTarget(new SignatureTarget(isCritical, keyAlgorithm.getAlgorithmId(), hashAlgorithm.getAlgorithmId(), hashData)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setSignatureTarget(@Nullable SignatureTarget signatureTarget) { + this.signatureTarget = signatureTarget; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setFeatures(Feature... features) { + return setFeatures(true, features); + } + + @Override + public SignatureSubpacketGeneratorWrapper setFeatures(boolean isCritical, Feature... features) { + byte bitmask = Feature.toBitmask(features); + return setFeatures(new Features(isCritical, bitmask)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setFeatures(@Nullable Features features) { + this.features = features; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper setTrust(int depth, int amount) { + return setTrust(true, depth, amount); + } + + @Override + public SignatureSubpacketGeneratorWrapper setTrust(boolean isCritical, int depth, int amount) { + return setTrust(new TrustSignature(isCritical, depth, amount)); + } + + @Override + public SignatureSubpacketGeneratorWrapper setTrust(@Nullable TrustSignature trust) { + this.trust = trust; + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper addEmbeddedSignature(@Nonnull PGPSignature signature) throws IOException { + return addEmbeddedSignature(true, signature); + } + + @Override + public SignatureSubpacketGeneratorWrapper addEmbeddedSignature(boolean isCritical, @Nonnull PGPSignature signature) throws IOException { + byte[] sig = signature.getEncoded(); + byte[] data; + + if (sig.length - 1 > 256) + { + data = new byte[sig.length - 3]; + } + else + { + data = new byte[sig.length - 2]; + } + + System.arraycopy(sig, sig.length - data.length, data, 0, data.length); + + return addEmbeddedSignature(new EmbeddedSignature(isCritical, false, data)); + } + + @Override + public SignatureSubpacketGeneratorWrapper addEmbeddedSignature(@Nonnull EmbeddedSignature embeddedSignature) { + this.embeddedSignatureList.add(embeddedSignature); + return this; + } + + @Override + public SignatureSubpacketGeneratorWrapper clearEmbeddedSignatures() { + this.embeddedSignatureList.clear(); + return this; + } + + private static void addSubpacket(PGPSignatureSubpacketGenerator generator, SignatureSubpacket subpacket) { + if (subpacket != null) { + generator.addCustomSubpacket(subpacket); + } + } +} diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapperTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapperTest.java new file mode 100644 index 00000000..709b424c --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/subpackets/SignatureSubpacketGeneratorWrapperTest.java @@ -0,0 +1,441 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.signature.subpackets; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +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.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Date; +import java.util.Iterator; +import java.util.Random; + +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.bcpg.sig.Exportable; +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.IssuerFingerprint; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.bcpg.sig.PreferredAlgorithms; +import org.bouncycastle.bcpg.sig.Revocable; +import org.bouncycastle.bcpg.sig.RevocationKey; +import org.bouncycastle.bcpg.sig.RevocationReason; +import org.bouncycastle.bcpg.sig.SignatureTarget; +import org.bouncycastle.bcpg.sig.TrustSignature; +import org.bouncycastle.openpgp.PGPException; +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.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.PublicKeyAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.algorithm.SymmetricKeyAlgorithm; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.key.util.RevocationAttributes; +import org.pgpainless.util.Passphrase; + +public class SignatureSubpacketGeneratorWrapperTest { + + private static PGPPublicKeyRing keys; + private static PGPPublicKey key; + + private SignatureSubpacketGeneratorWrapper wrapper; + + @BeforeAll + public static void setup() throws IOException { + keys = TestKeys.getEmilPublicKeyRing(); + key = keys.getPublicKey(); + } + + @BeforeEach + public void createWrapper() { + wrapper = new SignatureSubpacketGeneratorWrapper(key); + } + + @Test + public void initialStateTest() { + Date now = new Date(); + wrapper = new SignatureSubpacketGeneratorWrapper(); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(now.getTime(), vector.getSignatureCreationTime().getTime(), 1000); + } + + @Test + public void initialStateFromKeyTest() throws PGPException { + Date now = new Date(); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(key.getKeyID(), vector.getIssuerKeyID()); + assertEquals(key.getVersion(), vector.getIssuerFingerprint().getKeyVersion()); + assertArrayEquals(key.getFingerprint(), vector.getIssuerFingerprint().getFingerprint()); + assertEquals(now.getTime(), vector.getSignatureCreationTime().getTime(), 2000); + + assertEquals(0, vector.getKeyFlags()); + assertEquals(0, vector.getSignatureExpirationTime()); + assertNull(vector.getSignerUserID()); + assertFalse(vector.isPrimaryUserID()); + assertEquals(0, vector.getKeyExpirationTime()); + assertNull(vector.getPreferredCompressionAlgorithms()); + assertNull(vector.getPreferredSymmetricAlgorithms()); + assertNull(vector.getPreferredHashAlgorithms()); + assertEquals(0, vector.getNotationDataOccurrences().length); + assertNull(vector.getIntendedRecipientFingerprint()); + assertNull(vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE)); + assertNull(vector.getSubpacket(SignatureSubpacketTags.REVOCATION_KEY)); + assertNull(vector.getSubpacket(SignatureSubpacketTags.REVOCATION_REASON)); + assertNull(vector.getSignatureTarget()); + assertNull(vector.getFeatures()); + assertNull(vector.getSubpacket(SignatureSubpacketTags.TRUST_SIG)); + assertTrue(vector.getEmbeddedSignatures().isEmpty()); + } + + @Test + public void testNullKeyId() { + wrapper.setIssuerKeyId(null); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(0, vector.getIssuerKeyID()); + } + + + @Test + public void testNullFingerprint() { + wrapper.setIssuerFingerprint((IssuerFingerprint) null); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertNull(vector.getIssuerFingerprint()); + } + + @Test + public void testAddNotationData() { + wrapper.addNotationData(true, "critical@notation.data", "isCritical"); + wrapper.addNotationData(false, "noncrit@notation.data", "notCritical"); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + NotationData[] notationData = vector.getNotationDataOccurrences(); + assertEquals(2, notationData.length); + NotationData first = notationData[0]; + assertTrue(first.isCritical()); + assertTrue(first.isHumanReadable()); + assertEquals("critical@notation.data", first.getNotationName()); + assertEquals("isCritical", first.getNotationValue()); + + NotationData second = notationData[1]; + assertFalse(second.isCritical()); + assertTrue(second.isHumanReadable()); + assertEquals("noncrit@notation.data", second.getNotationName()); + assertEquals("notCritical", second.getNotationValue()); + + wrapper.clearNotationData(); + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getNotationDataOccurrences().length); + + } + + @Test + public void testIntendedRecipientFingerprints() { + wrapper.addIntendedRecipientFingerprint(key); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(1, vector.getSubpackets(SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT).length); + assertArrayEquals(key.getFingerprint(), vector.getIntendedRecipientFingerprint().getFingerprint()); + assertEquals(key.getVersion(), vector.getIntendedRecipientFingerprint().getKeyVersion()); + + wrapper.clearIntendedRecipientFingerprints(); + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT).length); + } + + @Test + public void testAddRevocationKeys() { + Iterator keyIterator = keys.getPublicKeys(); + PGPPublicKey first = keyIterator.next(); + wrapper.addRevocationKey(first); + assertTrue(keyIterator.hasNext()); + PGPPublicKey second = keyIterator.next(); + wrapper.addRevocationKey(false, true, second); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + SignatureSubpacket[] revKeys = vector.getSubpackets(SignatureSubpacketTags.REVOCATION_KEY); + assertEquals(2, revKeys.length); + RevocationKey r1 = (RevocationKey) revKeys[0]; + RevocationKey r2 = (RevocationKey) revKeys[1]; + + assertTrue(r1.isCritical()); + assertArrayEquals(first.getFingerprint(), r1.getFingerprint()); + assertEquals(first.getAlgorithm(), r1.getAlgorithm()); + assertEquals((byte) 0x80, r1.getSignatureClass()); + + assertFalse(r2.isCritical()); + assertArrayEquals(second.getFingerprint(), r2.getFingerprint()); + assertEquals(second.getAlgorithm(), r2.getAlgorithm()); + assertEquals((byte) (0x80 | 0x40), r2.getSignatureClass()); + + wrapper.clearRevocationKeys(); + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getSubpackets(SignatureSubpacketTags.REVOCATION_KEY).length); + } + + @Test + public void testSetKeyFlags() { + wrapper.setKeyFlags(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER, KeyFlag.SIGN_DATA); // duplicates are removed + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(KeyFlag.toBitmask(KeyFlag.SIGN_DATA, KeyFlag.CERTIFY_OTHER), vector.getKeyFlags()); + assertTrue(vector.getSubpacket(SignatureSubpacketTags.KEY_FLAGS).isCritical()); + } + + @Test + public void testSignatureExpirationTime() { + Date now = new Date(); + long secondsInAWeek = 60 * 60 * 24 * 7; + Date inAWeek = new Date(now.getTime() + 1000 * secondsInAWeek); + wrapper.setSignatureExpirationTime(now, inAWeek); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(secondsInAWeek, vector.getSignatureExpirationTime()); + } + + @Test + public void testSignatureExpirationTimeCannotBeNegative() { + Date now = new Date(); + long secondsInAWeek = 60 * 60 * 24 * 7; + Date oneWeekEarlier = new Date(now.getTime() - 1000 * secondsInAWeek); + assertThrows(IllegalArgumentException.class, () -> wrapper.setSignatureExpirationTime(now, oneWeekEarlier)); + } + + @Test + public void testSignerUserId() { + String userId = "Alice "; + wrapper.setSignerUserId(userId); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(userId, vector.getSignerUserID()); + } + + @Test + public void testSetPrimaryUserId() { + assertFalse(wrapper.getGenerator().generate().isPrimaryUserID()); + + wrapper.setPrimaryUserId(); + assertTrue(wrapper.getGenerator().generate().isPrimaryUserID()); + } + + @Test + public void testSetKeyExpiration() { + Date now = new Date(); + long secondsSinceKeyCreation = (now.getTime() - key.getCreationTime().getTime()) / 1000; + wrapper.setKeyExpirationTime(key, now); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(secondsSinceKeyCreation, vector.getKeyExpirationTime()); + } + + @Test + public void testSetKeyExpirationCannotBeNegative() { + Date beforeKeyCreation = new Date(key.getCreationTime().getTime() - 10000); + assertThrows(IllegalArgumentException.class, () -> wrapper.setKeyExpirationTime(key, beforeKeyCreation)); + } + + @Test + public void testSetPreferredCompressionAlgorithms() { + wrapper.setPreferredCompressionAlgorithms(CompressionAlgorithm.BZIP2, CompressionAlgorithm.ZIP, CompressionAlgorithm.BZIP2); // duplicates get removed + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + int[] ids = vector.getPreferredCompressionAlgorithms(); + assertEquals(2, ids.length); + assertEquals(CompressionAlgorithm.BZIP2.getAlgorithmId(), ids[0]); + assertEquals(CompressionAlgorithm.ZIP.getAlgorithmId(), ids[1]); + + wrapper.setPreferredCompressionAlgorithms(); // empty + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getPreferredCompressionAlgorithms().length); + + wrapper.setPreferredCompressionAlgorithms((PreferredAlgorithms) null); + vector = wrapper.getGenerator().generate(); + assertNull(vector.getPreferredCompressionAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredCompressionAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_SYM_ALGS, true, new int[0]))); + } + + @Test + public void testSetPreferredSymmetricKeyAlgorithms() { + wrapper.setPreferredSymmetricKeyAlgorithms(SymmetricKeyAlgorithm.AES_192, SymmetricKeyAlgorithm.AES_128, SymmetricKeyAlgorithm.AES_128); // duplicates get removed + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + int[] ids = vector.getPreferredSymmetricAlgorithms(); + assertEquals(2, ids.length); + assertEquals(SymmetricKeyAlgorithm.AES_192.getAlgorithmId(), ids[0]); + assertEquals(SymmetricKeyAlgorithm.AES_128.getAlgorithmId(), ids[1]); + + wrapper.setPreferredSymmetricKeyAlgorithms(); // empty + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getPreferredSymmetricAlgorithms().length); + + wrapper.setPreferredSymmetricKeyAlgorithms((PreferredAlgorithms) null); + vector = wrapper.getGenerator().generate(); + assertNull(vector.getPreferredCompressionAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredSymmetricKeyAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_HASH_ALGS, true, new int[0]))); + } + + @Test + public void testSetPreferredHashAlgorithms() { + wrapper.setPreferredHashAlgorithms(HashAlgorithm.SHA512, HashAlgorithm.SHA384, HashAlgorithm.SHA512); // duplicates get removed + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + int[] ids = vector.getPreferredHashAlgorithms(); + assertEquals(2, ids.length); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), ids[0]); + assertEquals(HashAlgorithm.SHA384.getAlgorithmId(), ids[1]); + + wrapper.setPreferredHashAlgorithms(); // empty + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getPreferredHashAlgorithms().length); + + wrapper.setPreferredHashAlgorithms((PreferredAlgorithms) null); + vector = wrapper.getGenerator().generate(); + assertNull(vector.getPreferredHashAlgorithms()); + + assertThrows(IllegalArgumentException.class, () -> wrapper.setPreferredHashAlgorithms( + new PreferredAlgorithms(SignatureSubpacketTags.PREFERRED_COMP_ALGS, true, new int[0]))); + } + + @Test + public void testSetExportable() { + wrapper.setExportable(true, false); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + Exportable exportable = (Exportable) vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE); + assertTrue(exportable.isCritical()); + assertFalse(exportable.isExportable()); + + wrapper.setExportable(false, true); + vector = wrapper.getGenerator().generate(); + + exportable = (Exportable) vector.getSubpacket(SignatureSubpacketTags.EXPORTABLE); + assertFalse(exportable.isCritical()); + assertTrue(exportable.isExportable()); + } + + @Test + public void testSetRevocable() { + wrapper.setRevocable(true, true); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + Revocable revocable = (Revocable) vector.getSubpacket(SignatureSubpacketTags.REVOCABLE); + assertTrue(revocable.isCritical()); + assertTrue(revocable.isRevocable()); + + wrapper.setRevocable(false, false); + vector = wrapper.getGenerator().generate(); + + revocable = (Revocable) vector.getSubpacket(SignatureSubpacketTags.REVOCABLE); + assertFalse(revocable.isCritical()); + assertFalse(revocable.isRevocable()); + } + + @Test + public void testSetRevocationReason() { + wrapper.setRevocationReason(RevocationAttributes.createKeyRevocation() + .withReason(RevocationAttributes.Reason.KEY_RETIRED).withDescription("The key is too weak.")); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + assertEquals(1, vector.getSubpackets(SignatureSubpacketTags.REVOCATION_REASON).length); + RevocationReason reason = (RevocationReason) vector.getSubpacket(SignatureSubpacketTags.REVOCATION_REASON); + assertEquals(RevocationAttributes.Reason.KEY_RETIRED.code(), reason.getRevocationReason()); + assertEquals("The key is too weak.", reason.getRevocationDescription()); + } + + @Test + public void testSetSignatureTarget() { + byte[] hash = new byte[20]; + new Random().nextBytes(hash); + wrapper.setSignatureTarget(PublicKeyAlgorithm.fromId(key.getAlgorithm()), HashAlgorithm.SHA512, hash); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + SignatureTarget target = vector.getSignatureTarget(); + assertNotNull(target); + assertEquals(key.getAlgorithm(), target.getPublicKeyAlgorithm()); + assertEquals(HashAlgorithm.SHA512.getAlgorithmId(), target.getHashAlgorithm()); + assertArrayEquals(hash, target.getHashData()); + } + + @Test + public void testSetFeatures() { + wrapper.setFeatures(Feature.MODIFICATION_DETECTION, Feature.AEAD_ENCRYPTED_DATA); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + Features features = vector.getFeatures(); + assertTrue(features.supportsModificationDetection()); + assertTrue(features.supportsFeature(Features.FEATURE_AEAD_ENCRYPTED_DATA)); + assertFalse(features.supportsFeature(Features.FEATURE_VERSION_5_PUBLIC_KEY)); + } + + @Test + public void testSetTrust() { + wrapper.setTrust(10, 5); + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + + TrustSignature trustSignature = (TrustSignature) vector.getSubpacket(SignatureSubpacketTags.TRUST_SIG); + assertNotNull(trustSignature); + assertEquals(10, trustSignature.getDepth()); + assertEquals(5, trustSignature.getTrustAmount()); + } + + @Test + public void testAddEmbeddedSignature() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + Iterator secretKeyIterator = secretKeys.iterator(); + PGPSecretKey primaryKey = secretKeyIterator.next(); + PGPSignatureGenerator generator = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder(primaryKey.getPublicKey().getAlgorithm(), HashAlgorithm.SHA512.getAlgorithmId()) + ); + + PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(primaryKey, (Passphrase) null); + generator.init(SignatureType.DIRECT_KEY.getCode(), privateKey); + PGPSignature sig1 = generator.generateCertification(primaryKey.getPublicKey()); + + generator.init(SignatureType.DIRECT_KEY.getCode(), privateKey); + PGPSignature sig2 = generator.generateCertification(secretKeyIterator.next().getPublicKey()); + + wrapper.addEmbeddedSignature(sig1); + + PGPSignatureSubpacketVector vector = wrapper.getGenerator().generate(); + assertEquals(1, vector.getEmbeddedSignatures().size()); + assertArrayEquals(sig1.getSignature(), vector.getEmbeddedSignatures().get(0).getSignature()); + + wrapper.addEmbeddedSignature(sig2); + + vector = wrapper.getGenerator().generate(); + assertEquals(2, vector.getEmbeddedSignatures().size()); + assertArrayEquals(sig2.getSignature(), vector.getEmbeddedSignatures().get(1).getSignature()); + + wrapper.clearEmbeddedSignatures(); + vector = wrapper.getGenerator().generate(); + assertEquals(0, vector.getEmbeddedSignatures().size()); + } +}