From bccf384dbf8791911cbb2e4625446f3c014db2c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Oct 2021 13:32:04 +0200 Subject: [PATCH] Add feature-related utilities and tests --- .../org/pgpainless/algorithm/Feature.java | 34 ++++ .../subpackets/SignatureSubpacketsUtil.java | 17 ++ .../SignatureSubpacketsUtilTest.java | 165 ++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java index 77f7ae65..ec090935 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/Feature.java @@ -15,6 +15,8 @@ */ package org.pgpainless.algorithm; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -34,6 +36,8 @@ public enum Feature { * RFC-4880 ยง5.14: Modification Detection Code Packet */ MODIFICATION_DETECTION(Features.FEATURE_MODIFICATION_DETECTION), + AEAD_ENCRYPTED_DATA(Features.FEATURE_AEAD_ENCRYPTED_DATA), + VERSION_5_PUBLIC_KEY(Features.FEATURE_VERSION_5_PUBLIC_KEY) ; private static final Map MAP = new ConcurrentHashMap<>(); @@ -57,4 +61,34 @@ public enum Feature { public byte getFeatureId() { return featureId; } + + /** + * Convert a bitmask into a list of {@link KeyFlag KeyFlags}. + * + * @param bitmask bitmask + * @return list of key flags encoded by the bitmask + */ + public static List fromBitmask(int bitmask) { + List features = new ArrayList<>(); + for (Feature f : Feature.values()) { + if ((bitmask & f.featureId) != 0) { + features.add(f); + } + } + return features; + } + + /** + * Encode a list of {@link KeyFlag KeyFlags} into a bitmask. + * + * @param features list of flags + * @return bitmask + */ + public static byte toBitmask(Feature... features) { + byte mask = 0; + for (Feature f : features) { + mask |= f.featureId; + } + return mask; + } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java index 19ec1b21..57b06c02 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java +++ b/pgpainless-core/src/main/java/org/pgpainless/signature/subpackets/SignatureSubpacketsUtil.java @@ -49,6 +49,7 @@ import org.bouncycastle.openpgp.PGPSignatureList; import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; import org.bouncycastle.util.encoders.Hex; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.KeyFlag; import org.pgpainless.algorithm.SignatureSubpacket; @@ -356,6 +357,22 @@ public final class SignatureSubpacketsUtil { return hashed(signature, SignatureSubpacket.features); } + /** + * Parse out the features subpacket of a signature. + * If the signature has no features subpacket, return null. + * Otherwise, return the features as a feature set. + * + * @param signature signature + * @return features as set + */ + public static @Nullable Set parseFeatures(PGPSignature signature) { + Features features = getFeatures(signature); + if (features == null) { + return null; + } + return new LinkedHashSet<>(Feature.fromBitmask(features.getData()[0])); + } + /** * Return the signature target subpacket from the signature. * We search for this subpacket in the hashed and unhashed area (in this order). diff --git a/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java new file mode 100644 index 00000000..34d82bfc --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/signature/SignatureSubpacketsUtilTest.java @@ -0,0 +1,165 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.signature; + +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.security.InvalidAlgorithmParameterException; +import java.security.NoSuchAlgorithmException; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPrivateKey; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPSecretKeyRing; +import org.bouncycastle.openpgp.PGPSignature; +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; +import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.Feature; +import org.pgpainless.algorithm.HashAlgorithm; +import org.pgpainless.algorithm.SignatureType; +import org.pgpainless.implementation.ImplementationFactory; +import org.pgpainless.key.TestKeys; +import org.pgpainless.key.protection.SecretKeyRingProtector; +import org.pgpainless.key.protection.UnlockSecretKey; +import org.pgpainless.policy.Policy; +import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil; + +public class SignatureSubpacketsUtilTest { + + @Test + public void test() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException { + PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing() + .modernKeyRing("Expire", null); + Date expiration = Date.from(new Date().toInstant().plus(365, ChronoUnit.DAYS)); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(expiration, SecretKeyRingProtector.unprotectedKeys()) + .done(); + + PGPSignature expirationSig = SignaturePicker.pickCurrentUserIdCertificationSignature(secretKeys, "Expire", Policy.getInstance(), new Date()); + PGPPublicKey notTheRightKey = PGPainless.inspectKeyRing(secretKeys).getSigningSubkeys().get(0); + + assertThrows(IllegalArgumentException.class, () -> + SignatureSubpacketsUtil.getKeyExpirationTimeAsDate(expirationSig, notTheRightKey)); + } + + @Test + public void testGetRevocable() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + + PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + PGPSignature withoutRevocable = generator.generateCertification(secretKeys.getPublicKey()); + assertNull(SignatureSubpacketsUtil.getRevocable(withoutRevocable)); + + generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); + hashed.setRevocable(true, true); + generator.setHashedSubpackets(hashed.generate()); + PGPSignature withRevocable = generator.generateCertification(secretKeys.getPublicKey()); + assertNotNull(SignatureSubpacketsUtil.getRevocable(withRevocable)); + } + + @Test + public void testParsePreferredCompressionAlgorithms() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + + PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + + PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); + Set compressionAlgorithmSet = new LinkedHashSet<>(Arrays.asList(CompressionAlgorithm.BZIP2, CompressionAlgorithm.ZIP)); + int[] ids = new int[compressionAlgorithmSet.size()]; + Iterator it = compressionAlgorithmSet.iterator(); + for (int i = 0; i < ids.length; i++) { + ids[i] = it.next().getAlgorithmId(); + } + hashed.setPreferredCompressionAlgorithms(true, ids); + generator.setHashedSubpackets(hashed.generate()); + + PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); + + Set parsed = SignatureSubpacketsUtil.parsePreferredCompressionAlgorithms(signature); + assertEquals(compressionAlgorithmSet, parsed); + } + + @Test + public void testParseKeyFlagsOfNullIsNull() { + assertNull(SignatureSubpacketsUtil.parseKeyFlags(null)); + } + + @Test + public void testParseKeyFlagsOfNullSubpacketIsNull() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + + PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + PGPSignature withoutKeyFlags = generator.generateCertification(secretKeys.getPublicKey()); + assertNull(SignatureSubpacketsUtil.parseKeyFlags(withoutKeyFlags)); + } + + @Test + public void testParseFeaturesIsNullForNullSubpacket() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + + PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + PGPSignature withoutKeyFlags = generator.generateCertification(secretKeys.getPublicKey()); + assertNull(SignatureSubpacketsUtil.parseFeatures(withoutKeyFlags)); + } + + @Test + public void testParseFeatures() throws PGPException, IOException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + PGPPrivateKey certKey = UnlockSecretKey.unlockSecretKey(secretKeys.getSecretKey(), SecretKeyRingProtector.unprotectedKeys()); + + PGPSignatureGenerator generator = getSignatureGenerator(certKey, SignatureType.CASUAL_CERTIFICATION); + PGPSignatureSubpacketGenerator hashed = new PGPSignatureSubpacketGenerator(); + hashed.setFeature(true, Feature.toBitmask(Feature.MODIFICATION_DETECTION, Feature.AEAD_ENCRYPTED_DATA)); + generator.setHashedSubpackets(hashed.generate()); + + PGPSignature signature = generator.generateCertification(secretKeys.getPublicKey()); + Set featureSet = SignatureSubpacketsUtil.parseFeatures(signature); + assertEquals(2, featureSet.size()); + assertTrue(featureSet.contains(Feature.MODIFICATION_DETECTION)); + assertTrue(featureSet.contains(Feature.AEAD_ENCRYPTED_DATA)); + assertFalse(featureSet.contains(Feature.VERSION_5_PUBLIC_KEY)); + } + + private PGPSignatureGenerator getSignatureGenerator(PGPPrivateKey signingKey, + SignatureType signatureType) throws PGPException { + PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator( + ImplementationFactory.getInstance().getPGPContentSignerBuilder( + signingKey.getPublicKeyPacket().getAlgorithm(), + HashAlgorithm.SHA512.getAlgorithmId())); + signatureGenerator.init(signatureType.getCode(), signingKey); + return signatureGenerator; + } +}