From 7ad1cb4169167fec3f3bcdc417a6051697985fba Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jan 2021 17:08:20 +0100 Subject: [PATCH 1/4] Add SignatureSubpacket enum --- .../algorithm/SignatureSubpacket.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java new file mode 100644 index 00000000..8ba5d75f --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/algorithm/SignatureSubpacket.java @@ -0,0 +1,115 @@ +/* + * 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.algorithm; + +import static org.bouncycastle.bcpg.SignatureSubpacketTags.ATTESTED_CERTIFICATIONS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.CREATION_TIME; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.EMBEDDED_SIGNATURE; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPIRE_TIME; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.EXPORTABLE; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.FEATURES; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.INTENDED_RECIPIENT_FINGERPRINT; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_FINGERPRINT; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.ISSUER_KEY_ID; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_EXPIRE_TIME; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_FLAGS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.KEY_SERVER_PREFS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.NOTATION_DATA; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PLACEHOLDER; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.POLICY_URL; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_AEAD_ALGORITHMS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_COMP_ALGS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_HASH_ALGS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_KEY_SERV; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PREFERRED_SYM_ALGS; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.PRIMARY_USER_ID; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.REG_EXP; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCABLE; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNATURE_TARGET; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.SIGNER_USER_ID; +import static org.bouncycastle.bcpg.SignatureSubpacketTags.TRUST_SIG; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public enum SignatureSubpacket { + signatureCreationTime(CREATION_TIME), + signatureExpirationTime(EXPIRE_TIME), + exportableCertification(EXPORTABLE), + trustSignature(TRUST_SIG), + regularExpression(REG_EXP), + revocable(REVOCABLE), + keyExpirationTime(KEY_EXPIRE_TIME), + placeholder(PLACEHOLDER), + preferredSymmetricAlgorithms(PREFERRED_SYM_ALGS), + revocationKey(REVOCATION_KEY), + issuerKeyId(ISSUER_KEY_ID), + notationData(NOTATION_DATA), + preferredHashAlgorithms(PREFERRED_HASH_ALGS), + preferredCompressionAlgorithms(PREFERRED_COMP_ALGS), + keyServerPreferences(KEY_SERVER_PREFS), + preferredKeyServers(PREFERRED_KEY_SERV), + primaryUserId(PRIMARY_USER_ID), + policyUrl(POLICY_URL), + keyFlags(KEY_FLAGS), + signerUserId(SIGNER_USER_ID), + revocationReason(REVOCATION_REASON), + features(FEATURES), + signatureTarget(SIGNATURE_TARGET), + embeddedSignature(EMBEDDED_SIGNATURE), + issuerFingerprint(ISSUER_FINGERPRINT), + preferredAEADAlgorithms(PREFERRED_AEAD_ALGORITHMS), + intendedRecipientFingerprint(INTENDED_RECIPIENT_FINGERPRINT), + attestedCertification(ATTESTED_CERTIFICATIONS) + ; + + private static final Map MAP = new ConcurrentHashMap<>(); + static { + for (SignatureSubpacket p : values()) { + MAP.put(p.code, p); + } + } + + private final int code; + + SignatureSubpacket(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public static SignatureSubpacket fromCode(int code) { + SignatureSubpacket tag = MAP.get(code); + if (tag == null) { + throw new IllegalArgumentException("No SignatureSubpacket tag found with code " + code); + } + return tag; + } + + public static List fromCodes(int[] codes) { + List tags = new ArrayList<>(); + for (int code : codes) { + tags.add(fromCode(code)); + } + return tags; + } +} From 21ba97c598deaf946429e4b7676fcc886dfbe5c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jan 2021 17:08:52 +0100 Subject: [PATCH 2/4] Add SubpacketInspector and SignatureSubpacketGeneratorUtil classes --- .../util/SignatureSubpacketGeneratorUtil.java | 87 ++++++++++ .../pgpainless/util/SubpacketsInspector.java | 159 ++++++++++++++++++ 2 files changed, 246 insertions(+) create mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/SignatureSubpacketGeneratorUtil.java create mode 100644 pgpainless-core/src/main/java/org/pgpainless/util/SubpacketsInspector.java diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SignatureSubpacketGeneratorUtil.java b/pgpainless-core/src/main/java/org/pgpainless/util/SignatureSubpacketGeneratorUtil.java new file mode 100644 index 00000000..560c217a --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SignatureSubpacketGeneratorUtil.java @@ -0,0 +1,87 @@ +/* + * 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.util; + +import java.util.Date; + +import javax.annotation.Nonnull; + +import org.bouncycastle.bcpg.SignatureSubpacket; +import org.bouncycastle.bcpg.SignatureSubpacketTags; +import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator; + +/** + * Utility class that helps dealing with BCs SignatureSubpacketGenerator class. + */ +public class SignatureSubpacketGeneratorUtil { + + public static void removeAllPacketsOfType(org.pgpainless.algorithm.SignatureSubpacket subpacketType, + PGPSignatureSubpacketGenerator subpacketGenerator) { + removeAllPacketsOfType(subpacketType.getCode(), subpacketGenerator); + } + + public static void removeAllPacketsOfType(int type, PGPSignatureSubpacketGenerator subpacketGenerator) { + for (SignatureSubpacket subpacket : subpacketGenerator.getSubpackets(type)) { + subpacketGenerator.removePacket(subpacket); + } + } + + /** + * Replace all occurrences of a signature creation time subpackets in the subpacket generator + * with a single new instance representing the provided date. + * + * @param date signature creation time + * @param subpacketGenerator subpacket generator + */ + public static void setSignatureCreationTimeInSubpacketGenerator(Date date, PGPSignatureSubpacketGenerator subpacketGenerator) { + removeAllPacketsOfType(SignatureSubpacketTags.CREATION_TIME, subpacketGenerator); + subpacketGenerator.setSignatureCreationTime(false, date); + } + + /** + * Replace all occurrences of key expiration time subpackets in the subpacket generator + * with a single instance representing the new expiration time. + * + * @param expirationDate expiration time as date or null for no expiration + * @param creationDate date on which the key was created + * @param subpacketGenerator subpacket generator + */ + public static void setExpirationDateInSubpacketGenerator(Date expirationDate, + @Nonnull Date creationDate, + PGPSignatureSubpacketGenerator subpacketGenerator) { + removeAllPacketsOfType(SignatureSubpacketTags.KEY_EXPIRE_TIME, subpacketGenerator); + long secondsToExpire = getKeyLifetimeInSeconds(expirationDate, creationDate); + subpacketGenerator.setKeyExpirationTime(true, secondsToExpire); + } + + /** + * Calculate the duration in seconds until the key expires after creation. + * + * @param expirationDate new expiration date + * @param creationTime key creation time + * @return life time of the key in seconds + */ + private static long getKeyLifetimeInSeconds(Date expirationDate, @Nonnull Date creationTime) { + long secondsToExpire = 0; // 0 means "no expiration" + if (expirationDate != null) { + if (creationTime.after(expirationDate)) { + throw new IllegalArgumentException("Key MUST NOT expire before being created. (creation: " + creationTime + ", expiration: " + expirationDate + ")"); + } + secondsToExpire = (expirationDate.getTime() - creationTime.getTime()) / 1000; + } + return secondsToExpire; + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/SubpacketsInspector.java b/pgpainless-core/src/main/java/org/pgpainless/util/SubpacketsInspector.java new file mode 100644 index 00000000..f19a1fca --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/util/SubpacketsInspector.java @@ -0,0 +1,159 @@ +/* + * 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.util; + +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import org.bouncycastle.bcpg.sig.Features; +import org.bouncycastle.bcpg.sig.IntendedRecipientFingerprint; +import org.bouncycastle.bcpg.sig.NotationData; +import org.bouncycastle.openpgp.PGPSignatureSubpacketVector; +import org.pgpainless.algorithm.KeyFlag; +import org.pgpainless.algorithm.SignatureSubpacket; +import org.pgpainless.key.OpenPgpV4Fingerprint; + +public class SubpacketsInspector { + + public static StringBuilder toString(PGPSignatureSubpacketVector vector) { + StringBuilder sb = new StringBuilder(); + optAppendSignatureCreationTime(sb, vector); + optAppendSignatureExpirationTime(sb, vector); + optAppendFlags(sb, vector); + optAppendFeatures(sb, vector); + optAppendIssuerKeyID(sb, vector); + optAppendSignerUserID(sb, vector); + optAppendKeyExpirationTime(sb, vector); + optAppendIntendedRecipientFingerprint(sb, vector); + optAppendNotationDataOccurrences(sb, vector); + optAppendCriticalTags(sb, vector); + return sb; + } + + private static StringBuilder optAppendCriticalTags(StringBuilder sb, PGPSignatureSubpacketVector v) { + int[] criticalTagCodes = v.getCriticalTags(); + if (criticalTagCodes.length == 0) { + return sb; + } + + sb.append("Critical Tags: ").append('['); + for (int i = 0; i < criticalTagCodes.length; i++) { + int tag = criticalTagCodes[i]; + try { + sb.append(SignatureSubpacket.fromCode(tag)).append(i == criticalTagCodes.length - 1 ? "" : ", "); + } catch (IllegalArgumentException e) { + + } + } + return sb.append(']').append('\n'); + } + + private static StringBuilder optAppendNotationDataOccurrences(StringBuilder sb, PGPSignatureSubpacketVector v) { + NotationData[] notationData = v.getNotationDataOccurrences(); + if (notationData.length == 0) { + return sb; + } + sb.append("Notation Data: [").append('\n'); + for (int i = 0; i < notationData.length; i++) { + NotationData n = notationData[i]; + sb.append('\'').append(n.getNotationName()) + .append("' = '").append(n.getNotationValue()) + .append(i == notationData.length - 1 ? "'" : "', "); + } + return sb.append('\n'); + } + + private static StringBuilder optAppendSignatureCreationTime(StringBuilder sb, PGPSignatureSubpacketVector v) { + return sb.append("Sig created: ").append(v.getSignatureCreationTime()).append('\n'); + } + + private static StringBuilder optAppendSignatureExpirationTime(StringBuilder sb, PGPSignatureSubpacketVector v) { + long time = v.getSignatureExpirationTime(); + sb.append("Sig expires: "); + if (time == 0) { + sb.append("never"); + } else { + Date creationTime = v.getSignatureCreationTime(); + if (creationTime != null) { + long seconds = creationTime.getTime() / 1000; + Date expirationDate = new Date((seconds + time) * 1000); + sb.append(expirationDate).append(" (").append(time).append(')'); + } else { + sb.append(time); + } + } + return sb.append('\n'); + } + + private static StringBuilder optAppendFlags(StringBuilder sb, PGPSignatureSubpacketVector v) { + List flagList = KeyFlag.fromBitmask(v.getKeyFlags()); + sb.append("Flags: ").append(Arrays.toString(flagList.toArray())).append('\n'); + return sb; + } + + private static StringBuilder optAppendFeatures(StringBuilder sb, PGPSignatureSubpacketVector v) { + Features features = v.getFeatures(); + if (features == null) { + return sb; + } + sb.append("Features: "); + sb.append('['); + if (features.supportsModificationDetection()) { + sb.append("Modification Detection"); + } + sb.append(']'); + return sb.append('\n'); + } + + private static StringBuilder optAppendIssuerKeyID(StringBuilder sb, PGPSignatureSubpacketVector v) { + long keyId = v.getIssuerKeyID(); + if (keyId == 0) { + return sb; + } + return sb.append("Issuer KeyID: ").append(Long.toHexString(keyId)).append('\n'); + } + + private static StringBuilder optAppendSignerUserID(StringBuilder sb, PGPSignatureSubpacketVector v) { + String userID = v.getSignerUserID(); + if (userID == null) { + return sb; + } + return sb.append("Signer UserID: ").append(userID).append('\n'); + } + + private static StringBuilder optAppendKeyExpirationTime(StringBuilder sb, PGPSignatureSubpacketVector v) { + long expirationTime = v.getKeyExpirationTime(); + sb.append("Key Expiration Time: "); + if (expirationTime == 0) { + sb.append("never"); + } else { + sb.append(expirationTime).append(" seconds after creation"); + } + return sb.append('\n'); + } + + private static StringBuilder optAppendIntendedRecipientFingerprint(StringBuilder sb, PGPSignatureSubpacketVector v) { + IntendedRecipientFingerprint fingerprint = v.getIntendedRecipientFingerprint(); + if (fingerprint == null) { + return sb; + } + return sb.append("Intended Recipient Fingerprint: ") + .append(new OpenPgpV4Fingerprint(fingerprint.getFingerprint())) + .append('\n'); + } + +} From bf8e29caa43d6b4ff0242cc25e096e73b5d4e88c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jan 2021 17:09:34 +0100 Subject: [PATCH 3/4] Add KeyRingInfo.getExpirationDate(fingerprint) to get subkey exp dates --- .../src/main/java/org/pgpainless/key/info/KeyRingInfo.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java index 635e5ae4..6671dcb9 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/info/KeyRingInfo.java @@ -215,7 +215,11 @@ public class KeyRingInfo { * @return expiration date */ public Date getExpirationDate() { - long validSeconds = getPublicKey().getValidSeconds(); + return getExpirationDate(new OpenPgpV4Fingerprint(getPublicKey())); + } + + public Date getExpirationDate(OpenPgpV4Fingerprint fingerprint) { + long validSeconds = keys.getPublicKey(fingerprint.getKeyId()).getValidSeconds(); if (validSeconds == 0) { return null; } From b25a78bc2991c86bb3d659dbe58de2c496b503d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 18 Jan 2021 17:09:57 +0100 Subject: [PATCH 4/4] Fix changing of expiration dates for keys and subkeys --- .../secretkeyring/SecretKeyRingEditor.java | 134 +++++++++++++----- .../SecretKeyRingEditorInterface.java | 6 +- .../modification/ChangeExpirationTest.java | 52 +++++-- 3 files changed, 148 insertions(+), 44 deletions(-) diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index b9a94686..72c1ab21 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -62,6 +62,7 @@ import org.pgpainless.key.util.KeyRingUtils; import org.pgpainless.key.util.RevocationAttributes; import org.pgpainless.key.util.SignatureUtils; import org.pgpainless.util.Passphrase; +import org.pgpainless.util.SignatureSubpacketGeneratorUtil; public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @@ -287,57 +288,120 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { return this; } + @Override + public SecretKeyRingEditorInterface setExpirationDate(Date expiration, + SecretKeyRingProtector secretKeyRingProtector) + throws PGPException { + return setExpirationDate(new OpenPgpV4Fingerprint(secretKeyRing), expiration, secretKeyRingProtector); + } + @Override public SecretKeyRingEditorInterface setExpirationDate(OpenPgpV4Fingerprint fingerprint, Date expiration, SecretKeyRingProtector secretKeyRingProtector) throws PGPException { - Iterator secretKeyIterator = secretKeyRing.getSecretKeys(); - - if (!secretKeyIterator.hasNext()) { - throw new NoSuchElementException("No secret keys in the ring."); - } - - PGPSecretKey secretKey = secretKeyIterator.next(); - PGPPublicKey publicKey = secretKey.getPublicKey(); - - if (!new OpenPgpV4Fingerprint(publicKey).equals(fingerprint)) { - throw new IllegalArgumentException("Currently it is possible to adjust expiration date for primary key only."); - } List secretKeyList = new ArrayList<>(); - PGPPrivateKey privateKey = unlockSecretKey(secretKey, secretKeyRingProtector); - PGPSecretKey primaryKey = secretKeyRing.getSecretKey(); - PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); - PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); - - long secondsToExpire = 0; // 0 means "no expiration" - if (expiration != null) { - secondsToExpire = (expiration.getTime() - primaryKey.getPublicKey().getCreationTime().getTime()) / 1000; - } - subpacketGenerator.setKeyExpirationTime(false, secondsToExpire); - - PGPSignatureSubpacketVector subPackets = subpacketGenerator.generate(); - signatureGenerator.setHashedSubpackets(subPackets); - - signatureGenerator.init(PGPSignature.POSITIVE_CERTIFICATION, privateKey); - - Iterator users = publicKey.getUserIDs(); - while (users.hasNext()) { - String user = users.next(); - PGPSignature signature = signatureGenerator.generateCertification(user, primaryKey.getPublicKey()); - publicKey = PGPPublicKey.addCertification(publicKey, user, signature); + if (!primaryKey.isMasterKey()) { + throw new IllegalArgumentException("Key Ring does not appear to contain a primary secret key."); } - secretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); - secretKeyList.add(secretKey); + boolean found = false; + Iterator iterator = secretKeyRing.iterator(); + while (iterator.hasNext()) { + PGPSecretKey secretKey = iterator.next(); + + // Skip over unaffected subkeys + if (secretKey.getKeyID() != fingerprint.getKeyId()) { + secretKeyList.add(secretKey); + continue; + } + // We found the target subkey + found = true; + secretKey = setExpirationDate(primaryKey, secretKey, expiration, secretKeyRingProtector); + secretKeyList.add(secretKey); + } + + if (!found) { + throw new IllegalArgumentException("Key Ring does not contain secret key with fingerprint " + fingerprint); + } secretKeyRing = new PGPSecretKeyRing(secretKeyList); return this; } + private PGPSecretKey setExpirationDate(PGPSecretKey primaryKey, + PGPSecretKey subjectKey, + Date expiration, + SecretKeyRingProtector secretKeyRingProtector) + throws PGPException { + + if (expiration != null && expiration.before(subjectKey.getPublicKey().getCreationTime())) { + throw new IllegalArgumentException("Expiration date cannot be before creation date."); + } + + PGPPrivateKey privateKey = KeyRingUtils.unlockSecretKey(primaryKey, secretKeyRingProtector); + PGPPublicKey subjectPubKey = subjectKey.getPublicKey(); + + PGPSignature oldSignature = getPreviousSignature(primaryKey, subjectPubKey); + + PGPSignatureSubpacketVector oldSubpackets = oldSignature.getHashedSubPackets(); + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(oldSubpackets); + SignatureSubpacketGeneratorUtil.setSignatureCreationTimeInSubpacketGenerator(new Date(), subpacketGenerator); + SignatureSubpacketGeneratorUtil.setExpirationDateInSubpacketGenerator(expiration, subjectPubKey.getCreationTime(), subpacketGenerator); + + PGPSignatureGenerator signatureGenerator = SignatureUtils.getSignatureGeneratorFor(primaryKey); + signatureGenerator.setHashedSubpackets(subpacketGenerator.generate()); + + if (primaryKey.getKeyID() == subjectKey.getKeyID()) { + signatureGenerator.init(PGPSignature.POSITIVE_CERTIFICATION, privateKey); + + for (Iterator it = subjectKey.getUserIDs(); it.hasNext(); ) { + String userId = it.next(); + PGPSignature signature = signatureGenerator.generateCertification(userId, subjectPubKey); + subjectPubKey = PGPPublicKey.addCertification(subjectPubKey, userId, signature); + } + } else { + signatureGenerator.init(PGPSignature.SUBKEY_BINDING, privateKey); + + PGPSignature signature = signatureGenerator.generateCertification(primaryKey.getPublicKey(), subjectPubKey); + subjectPubKey = PGPPublicKey.addCertification(subjectPubKey, signature); + } + + subjectKey = PGPSecretKey.replacePublicKey(subjectKey, subjectPubKey); + return subjectKey; + } + + private PGPSignature getPreviousSignature(PGPSecretKey primaryKey, PGPPublicKey subjectPubKey) { + PGPSignature oldSignature = null; + if (primaryKey.getKeyID() == subjectPubKey.getKeyID()) { + Iterator keySignatures = subjectPubKey.getSignaturesForKeyID(primaryKey.getKeyID()); + while (keySignatures.hasNext()) { + PGPSignature next = keySignatures.next(); + if (next.getSignatureType() == PGPSignature.POSITIVE_CERTIFICATION) { + oldSignature = next; + } + } + if (oldSignature == null) { + throw new IllegalStateException("Key " + new OpenPgpV4Fingerprint(subjectPubKey) + " does not have a previous positive signature."); + } + } else { + Iterator bindingSignatures = subjectPubKey.getSignaturesOfType(SignatureType.SUBKEY_BINDING.getCode()); + while (bindingSignatures.hasNext()) { + oldSignature = (PGPSignature) bindingSignatures.next(); + } + } + + if (oldSignature == null) { + throw new IllegalStateException("Key " + new OpenPgpV4Fingerprint(subjectPubKey) + " does not have a previous subkey binding signature."); + } + return oldSignature; + } + + + @Override public PGPSignature createRevocationCertificate(OpenPgpV4Fingerprint fingerprint, SecretKeyRingProtector secretKeyRingProtector, diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java index b0f39591..84e3b02f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditorInterface.java @@ -15,9 +15,9 @@ */ package org.pgpainless.key.modification.secretkeyring; -import java.util.Date; import java.security.InvalidAlgorithmParameterException; import java.security.NoSuchAlgorithmException; +import java.util.Date; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -198,6 +198,10 @@ public interface SecretKeyRingEditorInterface { RevocationAttributes revocationAttributes) throws PGPException; + SecretKeyRingEditorInterface setExpirationDate(Date expiration, + SecretKeyRingProtector secretKeyRingProtector) + throws PGPException; + /** * Set key expiration time. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java index 94cdcae3..b0b7d2ab 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/modification/ChangeExpirationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2020 Paul Schaub. + * 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. @@ -33,28 +33,64 @@ import java.util.Date; public class ChangeExpirationTest { - @Test - public void setExpirationDateAndThenUnsetIt() throws PGPException, IOException, InterruptedException { - PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + private final OpenPgpV4Fingerprint subKeyFingerprint = new OpenPgpV4Fingerprint("F73FDE6439ABE210B1AF4EDD273EF7A0C749807B"); + @Test + public void setExpirationDateAndThenUnsetIt_OnPrimaryKey() throws PGPException, IOException, InterruptedException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); - OpenPgpV4Fingerprint fingerprint = sInfo.getFingerprint(); assertNull(sInfo.getExpirationDate()); + assertNull(sInfo.getExpirationDate(subKeyFingerprint)); Date date = new Date(1606493432000L); - secretKeys = PGPainless.modifyKeyRing(secretKeys).setExpirationDate(fingerprint, date, new UnprotectedKeysProtector()).done(); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(date, new UnprotectedKeysProtector()).done(); sInfo = PGPainless.inspectKeyRing(secretKeys); assertNotNull(sInfo.getExpirationDate()); assertEquals(date.getTime(), sInfo.getExpirationDate().getTime()); + // subkey unchanged + assertNull(sInfo.getExpirationDate(subKeyFingerprint)); // We need to wait for one second as OpenPGP signatures have coarse-grained (up to a second) // accuracy. Creating two signatures within a short amount of time will make the second one // "invisible" - Thread.sleep(1000); + Thread.sleep(1100); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(null, new UnprotectedKeysProtector()).done(); - secretKeys = PGPainless.modifyKeyRing(secretKeys).setExpirationDate(fingerprint, null, new UnprotectedKeysProtector()).done(); sInfo = PGPainless.inspectKeyRing(secretKeys); assertNull(sInfo.getExpirationDate()); + assertNull(sInfo.getExpirationDate(subKeyFingerprint)); + } + + @Test + public void setExpirationDateAndThenUnsetIt_OnSubkey() throws PGPException, IOException, InterruptedException { + PGPSecretKeyRing secretKeys = TestKeys.getEmilSecretKeyRing(); + KeyRingInfo sInfo = PGPainless.inspectKeyRing(secretKeys); + + assertNull(sInfo.getExpirationDate(subKeyFingerprint)); + assertNull(sInfo.getExpirationDate()); + + Date date = new Date(1606493432000L); + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(subKeyFingerprint, date, new UnprotectedKeysProtector()).done(); + sInfo = PGPainless.inspectKeyRing(secretKeys); + assertNotNull(sInfo.getExpirationDate(subKeyFingerprint)); + assertEquals(date.getTime(), sInfo.getExpirationDate(subKeyFingerprint).getTime()); + assertNull(sInfo.getExpirationDate()); + + // We need to wait for one second as OpenPGP signatures have coarse-grained (up to a second) + // accuracy. Creating two signatures within a short amount of time will make the second one + // "invisible" + Thread.sleep(1100); + + secretKeys = PGPainless.modifyKeyRing(secretKeys) + .setExpirationDate(subKeyFingerprint, null, new UnprotectedKeysProtector()).done(); + + sInfo = PGPainless.inspectKeyRing(secretKeys); + assertNull(sInfo.getExpirationDate(subKeyFingerprint)); + assertNull(sInfo.getExpirationDate()); } }