1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-06-18 09:34:51 +02:00

Add documentation to signature related classes

This commit is contained in:
Paul Schaub 2021-05-03 13:37:47 +02:00
parent ec85f53bb6
commit 431a65517e
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
11 changed files with 587 additions and 44 deletions

View file

@ -118,7 +118,7 @@ public class SignatureVerifyingInputStream extends FilterInputStream {
signatureStructureIsAcceptable(signingKey, policy).verify(signature);
signatureIsEffective(new Date()).verify(signature);
SignatureChainValidator.validateSigningKey(signature, onePassSignature.getVerificationKeys(), PGPainless.getPolicy(), signature.getCreationTime());
SignatureChainValidator.validateSigningKey(signature, onePassSignature.getVerificationKeys(), PGPainless.getPolicy());
} catch (SignatureValidationException e) {
throw new SignatureException("Signature key is not valid.", e);

View file

@ -20,38 +20,79 @@ import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.SubkeyIdentifier;
/**
* Tuple-class which bundles together a signature, the signing key that created the signature,
* an identifier of the signing key and a record of whether or not the signature was verified.
*/
public class DetachedSignature {
private final PGPSignature signature;
private final PGPKeyRing signingKeyRing;
private final SubkeyIdentifier signingKeyIdentifier;
private boolean verified;
/**
* Create a new {@link DetachedSignature} object.
*
* @param signature signature
* @param signingKeyRing signing key that created the signature
* @param signingKeyIdentifier identifier of the used signing key
*/
public DetachedSignature(PGPSignature signature, PGPKeyRing signingKeyRing, SubkeyIdentifier signingKeyIdentifier) {
this.signature = signature;
this.signingKeyRing = signingKeyRing;
this.signingKeyIdentifier = signingKeyIdentifier;
}
/**
* Mark this {@link DetachedSignature} as verified.
*
* @param verified verified
*/
public void setVerified(boolean verified) {
this.verified = verified;
}
/**
* Return true iff the signature is verified.
*
* @return verified
*/
public boolean isVerified() {
return verified;
}
/**
* Return the OpenPGP signature.
*
* @return signature
*/
public PGPSignature getSignature() {
return signature;
}
/**
* Return an identifier pointing to the exact signing key which was used to create this signature.
*
* @return signing key identifier
*/
public SubkeyIdentifier getSigningKeyIdentifier() {
return signingKeyIdentifier;
}
/**
* Return the key ring that contains the signing key that created this signature.
*
* @return key ring
*/
public PGPKeyRing getSigningKeyRing() {
return signingKeyRing;
}
/**
* Return the {@link OpenPgpV4Fingerprint} of the key that created the signature.
*
* @return fingerprint of the signing key
*/
@Deprecated
public OpenPgpV4Fingerprint getFingerprint() {
return signingKeyIdentifier.getSubkeyFingerprint();

View file

@ -21,29 +21,65 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.pgpainless.key.OpenPgpV4Fingerprint;
/**
* Tuple-class that bundles together a {@link PGPOnePassSignature} object, a {@link PGPPublicKeyRing}
* destined to verify the signature, the {@link PGPSignature} itself and a record of whether or not the signature
* was verified.
*/
public class OnePassSignature {
private final PGPOnePassSignature onePassSignature;
private final PGPPublicKeyRing verificationKeys;
private PGPSignature signature;
private boolean verified;
/**
* Create a new {@link OnePassSignature}.
*
* @param onePassSignature one-pass signature packet used to initialize the signature verifier.
* @param verificationKeys verification keys
*/
public OnePassSignature(PGPOnePassSignature onePassSignature, PGPPublicKeyRing verificationKeys) {
this.onePassSignature = onePassSignature;
this.verificationKeys = verificationKeys;
}
/**
* Return true if the signature is verified.
*
* @return verified
*/
public boolean isVerified() {
return verified;
}
/**
* Return the {@link PGPOnePassSignature} object.
*
* @return onePassSignature
*/
public PGPOnePassSignature getOnePassSignature() {
return onePassSignature;
}
/**
* Return the {@link OpenPgpV4Fingerprint} of the signing key.
*
* @return signing key fingerprint
*/
public OpenPgpV4Fingerprint getFingerprint() {
return new OpenPgpV4Fingerprint(verificationKeys.getPublicKey(onePassSignature.getKeyID()));
}
/**
* Verify the one-pass signature.
* Note: This method only checks if the signature itself is correct.
* It does not check if the signing key was eligible to create the signature, or if the signature is expired etc.
* Those checks are being done by {@link org.pgpainless.decryption_verification.SignatureVerifyingInputStream}.
*
* @param signature parsed-out signature
* @return true if the signature was verified, false otherwise
* @throws PGPException if signature verification fails with an exception.
*/
public boolean verify(PGPSignature signature) throws PGPException {
this.verified = getOnePassSignature().verify(signature);
if (verified) {
@ -52,10 +88,20 @@ public class OnePassSignature {
return verified;
}
/**
* Return the signature.
*
* @return signature
*/
public PGPSignature getSignature() {
return signature;
}
/**
* Return the key ring used to verify the signature.
*
* @return verification keys
*/
public PGPPublicKeyRing getVerificationKeys() {
return verificationKeys;
}

View file

@ -31,20 +31,18 @@ import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
/**
* Utility class to select signatures from keys based on certain criteria.
* This abstract class provides a method {@link #accept(PGPSignature, PGPPublicKey, PGPKeyRing)} which shall only
* return true if the provided signature is acceptable regarding the implementations selection criteria.
*
* The idea is to create an implementation of the class for each criterion, so that those criteria can be
* composed to create complex validity checks.
*/
public abstract class SelectSignatureFromKey {
private static final Logger LOGGER = Logger.getLogger(SelectSignatureFromKey.class.getName());
public static SelectSignatureFromKey isValidAt(Date validationDate) {
return new SelectSignatureFromKey() {
@Override
public boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing) {
Date expirationDate = SignatureUtils.getSignatureExpirationDate(signature);
return !signature.getCreationTime().after(validationDate) && (expirationDate == null || expirationDate.after(validationDate));
}
};
}
public abstract boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing);
public List<PGPSignature> select(List<PGPSignature> signatures, PGPPublicKey key, PGPKeyRing keyRing) {
@ -57,6 +55,36 @@ public abstract class SelectSignatureFromKey {
return selected;
}
/**
* Criterion that checks if the signature is valid at the validation date.
* A signature is not valid if it was created after the validation date, or if it is expired at the validation date.
*
* creationTime <= validationDate < expirationDate.
*
* @param validationDate validation date
* @return criterion implementation
*/
public static SelectSignatureFromKey isValidAt(Date validationDate) {
return new SelectSignatureFromKey() {
@Override
public boolean accept(PGPSignature signature, PGPPublicKey key, PGPKeyRing keyRing) {
Date expirationDate = SignatureUtils.getSignatureExpirationDate(signature);
return !signature.getCreationTime().after(validationDate) && (expirationDate == null || expirationDate.after(validationDate));
}
};
}
/**
* Criterion that checks if the provided signature is a valid subkey binding signature.
*
* A signature is only a valid subkey binding signature if it is of type {@link SignatureType#SUBKEY_BINDING},
* if it was created by the primary key, and - if the subkey is capable of signing - it contains a valid
* primary key binding signature.
*
* @param primaryKey primary key
* @param subkey subkey
* @return criterion to validate binding signatures
*/
public static SelectSignatureFromKey isValidSubkeyBindingSignature(PGPPublicKey primaryKey, PGPPublicKey subkey) {
return new SelectSignatureFromKey() {
@Override
@ -70,16 +98,20 @@ public abstract class SelectSignatureFromKey {
return false;
}
boolean subkeyBindingSigValid;
try {
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey);
subkeyBindingSigValid = signature.verifyCertification(primaryKey, subkey);
} catch (PGPException e) {
LOGGER.log(Level.INFO, "Verification of subkey binding signature failed.", e);
if (!isSigNotExpired().accept(signature, subkey, keyRing)) {
LOGGER.log(Level.INFO, "Subkey binding signature expired.");
return false;
}
if (!subkeyBindingSigValid) {
// Check signature correctness
try {
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), primaryKey);
boolean subkeyBindingSigValid = signature.verifyCertification(primaryKey, subkey);
if (!subkeyBindingSigValid) {
return false;
}
} catch (PGPException e) {
LOGGER.log(Level.INFO, "Verification of subkey binding signature failed.", e);
return false;
}
@ -96,6 +128,13 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that checks if a primary key binding signature is valid.
*
* @param subkey subkey
* @param primaryKey primary key
* @return criterion to validate primary key binding signatures
*/
public static SelectSignatureFromKey isValidPrimaryKeyBindingSignature(PGPPublicKey subkey, PGPPublicKey primaryKey) {
return new SelectSignatureFromKey() {
@Override
@ -118,6 +157,7 @@ public abstract class SelectSignatureFromKey {
return false;
}
// Check signature correctness
try {
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), subkey);
return signature.verifyCertification(primaryKey, subkey);
@ -128,6 +168,12 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that checks if a signature has an embedded valid primary key binding signature.
* @param subkey subkey
* @param primaryKey primary key
* @return criterion
*/
public static SelectSignatureFromKey hasValidPrimaryKeyBindingSignatureSubpacket(PGPPublicKey subkey, PGPPublicKey primaryKey) {
return new SelectSignatureFromKey() {
@Override
@ -149,6 +195,14 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that checks if a signature is a valid v4 direct-key signature.
* Note: This method does not check expiration.
*
* @param signer signing key
* @param signee signed key
* @return criterion
*/
public static SelectSignatureFromKey isValidDirectKeySignature(PGPPublicKey signer, PGPPublicKey signee) {
return new SelectSignatureFromKey() {
@Override
@ -161,6 +215,7 @@ public abstract class SelectSignatureFromKey {
return false;
}
// Check signature correctness
try {
signature.init(ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider(), signer);
return signature.verifyCertification(signee);
@ -171,6 +226,12 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that checks if a signature is a valid key revocation signature.
*
* @param key primary key
* @return criterion
*/
public static SelectSignatureFromKey isValidKeyRevocationSignature(PGPPublicKey key) {
return and(
isVersion4Signature(),
@ -182,6 +243,11 @@ public abstract class SelectSignatureFromKey {
);
}
/**
* Criterion that only accepts valid subkey revocation signatures.
*
* @return criterion
*/
public static SelectSignatureFromKey isValidSubkeyRevocationSignature() {
return new SelectSignatureFromKey() {
@Override
@ -192,6 +258,13 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts valid subkey revocation signatures.
*
* @param subkey subkey
* @param primaryKey primary key
* @return criterion
*/
public static SelectSignatureFromKey isValidSubkeyRevocationSignature(PGPPublicKey subkey, PGPPublicKey primaryKey) {
return SelectSignatureFromKey.and(
isVersion4Signature(),
@ -201,6 +274,13 @@ public abstract class SelectSignatureFromKey {
);
}
/**
* Criterion that only accepts signatures which are valid user-id revocations.
*
* @param revoker signing key
* @param userId user id
* @return criterion
*/
public static SelectSignatureFromKey isValidCertificationRevocationSignature(PGPPublicKey revoker, String userId) {
return and(
isVersion4Signature(),
@ -210,6 +290,14 @@ public abstract class SelectSignatureFromKey {
);
}
/**
* Criterion that only accepts signatures which are valid signatures over a user-id.
* This method only checks signature correctness, not expiry etc.
*
* @param userId user-id
* @param signingKey signing key
* @return criterion
*/
public static SelectSignatureFromKey isValidSignatureOnUserId(String userId, PGPPublicKey signingKey) {
return new SelectSignatureFromKey() {
@Override
@ -225,6 +313,14 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures which are valid signatures over a key.
* This method only checks signature correctness, not expiry etc.
*
* @param target signed key
* @param signer signing key
* @return criterion
*/
public static SelectSignatureFromKey isVerifyingSignatureOnKey(PGPPublicKey target, PGPPublicKey signer) {
return new SelectSignatureFromKey() {
@Override
@ -241,6 +337,15 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures which are correct binding signatures.
* This method only checks signature correctness, not expiry etc.
*
* @param primaryKey primary key
* @param subkey subkey
* @param signingKey signing key (either primary, or subkey)
* @return criterion
*/
public static SelectSignatureFromKey isVerifyingSignatureOnKeys(PGPPublicKey primaryKey, PGPPublicKey subkey, PGPPublicKey signingKey) {
if (signingKey.getKeyID() != primaryKey.getKeyID() && signingKey.getKeyID() != subkey.getKeyID()) {
throw new IllegalArgumentException("Signing key MUST be either the primary or subkey.");
@ -259,6 +364,17 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts certification signatures.
*
* Those are signature of the following types:
* - {@link SignatureType#NO_CERTIFICATION},
* - {@link SignatureType#CASUAL_CERTIFICATION},
* - {@link SignatureType#GENERIC_CERTIFICATION},
* - {@link SignatureType#POSITIVE_CERTIFICATION}.
*
* @return criterion
*/
public static SelectSignatureFromKey isCertification() {
return new SelectSignatureFromKey() {
@Override
@ -268,6 +384,13 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts "well formed" signatures.
* A signature is "well formed", iff it has a creation time subpacket and if it does not predate
* its creating keys creation time.
*
* @return criterion
*/
public static SelectSignatureFromKey isWellFormed() {
return and(
hasCreationTimeSubpacket(),
@ -275,10 +398,21 @@ public abstract class SelectSignatureFromKey {
);
}
/**
* Criterion that only accepts v4 signatures.
*
* @return criterion
*/
public static SelectSignatureFromKey isVersion4Signature() {
return isVersion(4);
}
/**
* Criterion that only accepts signatures which carry a creation time subpacket.
* According to the RFC, all signatures are required to have such a subpacket.
*
* @return criterion
*/
public static SelectSignatureFromKey hasCreationTimeSubpacket() {
return new SelectSignatureFromKey() {
@Override
@ -288,10 +422,22 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures that were created by the provided key.
*
* @param publicKey public key of the creation key pair
* @return criterion
*/
public static SelectSignatureFromKey isCreatedBy(PGPPublicKey publicKey) {
return isCreatedBy(publicKey.getKeyID());
}
/**
* Criterion that only accepts signatures which were created by the public key with the provided key id.
*
* @param keyId key id
* @return criterion
*/
public static SelectSignatureFromKey isCreatedBy(long keyId) {
return new SelectSignatureFromKey() {
@Override
@ -301,10 +447,21 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures which are not expired RIGHT NOW.
*
* @return criterion
*/
public static SelectSignatureFromKey isSigNotExpired() {
return isSigNotExpired(new Date());
}
/**
* Criterion that only accepts signatures which are not expired at comparisonDate.
*
* @param comparisonDate comparison date
* @return criterion
*/
public static SelectSignatureFromKey isSigNotExpired(Date comparisonDate) {
return new SelectSignatureFromKey() {
@Override
@ -314,6 +471,11 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures which do not predate their signing key's creation date.
*
* @return criterion
*/
public static SelectSignatureFromKey doesNotPredateKeyCreationDate() {
return new SelectSignatureFromKey() {
@Override
@ -327,6 +489,12 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures which do not predate the creation date of the provided key.
*
* @param creator key
* @return criterion
*/
public static SelectSignatureFromKey doesNotPredateKeyCreationDate(PGPPublicKey creator) {
return new SelectSignatureFromKey() {
@Override
@ -336,6 +504,12 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures of the provided signature version.
*
* @param version signature version
* @return criterion
*/
public static SelectSignatureFromKey isVersion(int version) {
return new SelectSignatureFromKey() {
@Override
@ -345,6 +519,12 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Criterion that only accepts signatures that are of the provided {@link SignatureType}.
*
* @param signatureType signature type that shall be accepted
* @return criterion
*/
public static SelectSignatureFromKey isOfType(SignatureType signatureType) {
return new SelectSignatureFromKey() {
@Override
@ -354,6 +534,13 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Compose different {@link SelectSignatureFromKey} by combining them with a logic AND.
* A signature will only be accepted, iff it satisfies every selector from selectors.
*
* @param selectors one or more selectors
* @return combined selector using AND operator
*/
public static SelectSignatureFromKey and(SelectSignatureFromKey... selectors) {
return new SelectSignatureFromKey() {
@Override
@ -368,6 +555,13 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Compose different {@link SelectSignatureFromKey} by combining them with a logic OR.
* A signature will only be accepted, iff it satisfies at least one selector from selectors.
*
* @param selectors one or more selectors
* @return combined selector using OR operator
*/
public static SelectSignatureFromKey or(SelectSignatureFromKey... selectors) {
return new SelectSignatureFromKey() {
@Override
@ -381,6 +575,14 @@ public abstract class SelectSignatureFromKey {
};
}
/**
* Negate the result of a {@link SelectSignatureFromKey} implementations {@link #accept(PGPSignature, PGPPublicKey, PGPKeyRing)}.
* The resulting {@link SelectSignatureFromKey} will only accept signatures that are rejected by the provided selector
* and reject those that are accepted by it.
*
* @param selector selector whose logic operation will be negated
* @return negated selector
*/
public static SelectSignatureFromKey not(SelectSignatureFromKey selector) {
return new SelectSignatureFromKey() {
@Override

View file

@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bouncycastle.bcpg.sig.SignerUserID;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
@ -35,11 +36,32 @@ import org.pgpainless.exception.SignatureValidationException;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
/**
* This class implements validity checks on OpenPGP signatures.
* Its responsibilities are checking if a signing key was eligible to create a certain signature
* and if the signature is valid at the time of validation.
*/
public class SignatureChainValidator {
private static final Logger LOGGER = Logger.getLogger(SignatureChainValidator.class.getName());
public static boolean validateSigningKey(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy, Date validationDate) throws SignatureValidationException {
/**
* Check if the signing key was eligible to create the provided signature.
*
* That entails:
* - Check, if the primary key is being revoked via key-revocation signatures.
* - Check, if the keys user-ids are revoked or not bound.
* - Check, if the signing subkey is revoked or expired.
* - Check, if the signing key is not capable of signing
*
* @param signature signature
* @param signingKeyRing signing key ring
* @param policy validation policy
* @return true if the signing key was eligible to create the signature
* @throws SignatureValidationException in case of a validation constraint violation
*/
public static boolean validateSigningKey(PGPSignature signature, PGPPublicKeyRing signingKeyRing, Policy policy)
throws SignatureValidationException {
Map<PGPSignature, Exception> rejections = new ConcurrentHashMap<>();
@ -50,6 +72,7 @@ public class SignatureChainValidator {
PGPPublicKey primaryKey = signingKeyRing.getPublicKey();
// Key-Revocation Signatures
List<PGPSignature> directKeySignatures = new ArrayList<>();
Iterator<PGPSignature> primaryKeyRevocationIterator = primaryKey.getSignaturesOfType(SignatureType.KEY_REVOCATION.getCode());
while (primaryKeyRevocationIterator.hasNext()) {
@ -64,6 +87,7 @@ public class SignatureChainValidator {
}
}
// Direct-Key Signatures
Iterator<PGPSignature> keySignatures = primaryKey.getSignaturesOfType(SignatureType.DIRECT_KEY.getCode());
while (keySignatures.hasNext()) {
PGPSignature keySignature = keySignatures.next();
@ -78,14 +102,13 @@ public class SignatureChainValidator {
}
Collections.sort(directKeySignatures, new SignatureValidityComparator(SignatureCreationDateComparator.Order.NEW_TO_OLD));
if (directKeySignatures.isEmpty()) {
} else {
if (!directKeySignatures.isEmpty()) {
if (directKeySignatures.get(0).getSignatureType() == SignatureType.KEY_REVOCATION.getCode()) {
throw new SignatureValidationException("Primary key has been revoked.");
}
}
// User-ID signatures (certifications, revocations)
Iterator<String> userIds = primaryKey.getUserIDs();
Map<String, List<PGPSignature>> userIdSignatures = new ConcurrentHashMap<>();
while (userIds.hasNext()) {
@ -107,23 +130,39 @@ public class SignatureChainValidator {
userIdSignatures.put(userId, signaturesOnUserId);
}
boolean userIdValid = false;
boolean anyUserIdValid = false;
for (String userId : userIdSignatures.keySet()) {
if (!userIdSignatures.get(userId).isEmpty()) {
PGPSignature current = userIdSignatures.get(userId).get(0);
if (current.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) {
LOGGER.log(Level.FINE, "User-ID '" + userId + "' is revoked.");
} else {
userIdValid = true;
anyUserIdValid = true;
}
}
}
if (!userIdValid) {
throw new SignatureValidationException("Key is not valid at this point.", rejections);
if (!anyUserIdValid) {
throw new SignatureValidationException("No valid user-id found.", rejections);
}
if (signingSubkey != primaryKey) {
// Specific signer user-id
SignerUserID signerUserID = SignatureSubpacketsUtil.getSignerUserID(signature);
if (signerUserID != null) {
PGPSignature userIdSig = userIdSignatures.get(signerUserID.getID()).get(0);
if (userIdSig.getSignatureType() == SignatureType.CERTIFICATION_REVOCATION.getCode()) {
throw new SignatureValidationException("Signature was made with user-id '" + signerUserID.getID() + "' which is revoked.");
}
}
if (signingSubkey == primaryKey) {
if (!directKeySignatures.isEmpty()) {
if (KeyFlag.hasKeyFlag(SignatureSubpacketsUtil.getKeyFlags(directKeySignatures.get(0)).getFlags(), KeyFlag.SIGN_DATA)) {
return true;
}
}
} // Subkey Binding Signatures / Subkey Revocation Signatures
else {
List<PGPSignature> subkeySigs = new ArrayList<>();
Iterator<PGPSignature> bindingRevocations = signingSubkey.getSignaturesOfType(SignatureType.SUBKEY_REVOCATION.getCode());
while (bindingRevocations.hasNext()) {
@ -168,14 +207,41 @@ public class SignatureChainValidator {
return true;
}
public static boolean validateSignatureChain(PGPSignature signature, InputStream signedData, PGPPublicKeyRing signingKeyRing, Policy policy, Date validationDate)
/**
* Validate the given signing key and then verify the given signature while parsing out the signed data.
* Uninitialized means that no signed data has been read and the hash generators state has not yet been updated.
*
* @param signature uninitialized signature
* @param signedData input stream containing signed data
* @param signingKeyRing key ring containing signing key
* @param policy validation policy
* @param validationDate date of validation
* @return true if the signature is valid, false otherwise
* @throws SignatureValidationException for validation constraint violations
*/
public static boolean validateSignatureChain(PGPSignature signature,
InputStream signedData,
PGPPublicKeyRing signingKeyRing,
Policy policy,
Date validationDate)
throws SignatureValidationException {
validateSigningKey(signature, signingKeyRing, policy, validationDate);
validateSigningKey(signature, signingKeyRing, policy);
return SignatureValidator.verifyUninitializedSignature(signature, signedData, signingKeyRing.getPublicKey(signature.getKeyID()), policy, validationDate);
}
public static boolean validateSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy) throws SignatureValidationException {
validateSigningKey(signature, verificationKeys, policy, signature.getCreationTime());
/**
* Validate the signing key and the given initialized signature.
* Initialized means that the signatures hash generator has already been updated by reading the signed data completely.
*
* @param signature initialized signature
* @param verificationKeys key ring containing the verification key
* @param policy validation policy
* @return true if the signature is valid, false otherwise
* @throws SignatureValidationException in case of a validation constraint violation
*/
public static boolean validateSignature(PGPSignature signature, PGPPublicKeyRing verificationKeys, Policy policy)
throws SignatureValidationException {
validateSigningKey(signature, verificationKeys, policy);
PGPPublicKey signingKey = verificationKeys.getPublicKey(signature.getKeyID());
SignatureValidator.verifyInitializedSignature(signature, signingKey, policy, signature.getCreationTime());
return true;

View file

@ -19,21 +19,38 @@ import java.util.Comparator;
import org.bouncycastle.openpgp.PGPSignature;
/**
* Comparator which can be used to sort signatures with regard to their creation time.
*/
public class SignatureCreationDateComparator implements Comparator<PGPSignature> {
public static final Order DEFAULT_ORDER = Order.OLD_TO_NEW;
public enum Order {
/**
* Oldest signatures first.
*/
OLD_TO_NEW,
/**
* Newest signatures first.
*/
NEW_TO_OLD
}
private final Order order;
/**
* Create a new comparator which sorts signatures old to new.
*/
public SignatureCreationDateComparator() {
this(DEFAULT_ORDER);
}
/**
* Create a new comparator which sorts signatures according to the passed ordering.
* @param order ordering
*/
public SignatureCreationDateComparator(Order order) {
this.order = order;
}

View file

@ -31,7 +31,7 @@ import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.SignatureType;
@ -39,38 +39,89 @@ import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.util.KeyRingUtils;
import org.pgpainless.key.util.OpenPgpKeyAttributeUtil;
import org.pgpainless.key.util.RevocationAttributes;
import org.pgpainless.policy.Policy;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
/**
* Utility methods related to signatures.
*/
public class SignatureUtils {
/**
* Return a signature generator for the provided signing key.
* The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm.
*
* @param singingKey signing key
* @return signature generator
*/
public static PGPSignatureGenerator getSignatureGeneratorFor(PGPSecretKey singingKey) {
return getSignatureGeneratorFor(singingKey.getPublicKey());
}
/**
* Return a signature generator for the provided signing key.
* The signature generator will follow the hash algorithm preferences of the signing key and pick the best algorithm.
*
* @param signingPubKey signing key
* @return signature generator
*/
public static PGPSignatureGenerator getSignatureGeneratorFor(PGPPublicKey signingPubKey) {
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
getPgpContentSignerBuilderForKey(signingPubKey));
return signatureGenerator;
}
private static BcPGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) {
/**
* Return a content signer builder fot the passed public key.
*
* The content signer will use a hash algorithm derived from the keys algorithm preferences.
* If no preferences can be derived, the key will fall back to the default hash algorithm as set in
* the {@link org.pgpainless.policy.Policy}.
*
* @param publicKey public key
* @return content signer builder
*/
private static PGPContentSignerBuilder getPgpContentSignerBuilderForKey(PGPPublicKey publicKey) {
List<HashAlgorithm> preferredHashAlgorithms = OpenPgpKeyAttributeUtil.getPreferredHashAlgorithms(publicKey);
if (preferredHashAlgorithms.isEmpty()) {
preferredHashAlgorithms = OpenPgpKeyAttributeUtil.guessPreferredHashAlgorithms(publicKey);
}
HashAlgorithm hashAlgorithm = negotiateHashAlgorithm(preferredHashAlgorithms);
return new BcPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId());
return ImplementationFactory.getInstance().getPGPContentSignerBuilder(publicKey.getAlgorithm(), hashAlgorithm.getAlgorithmId());
}
/**
* Negotiate an acceptable hash algorithm from the provided list of options.
* Acceptance of hash algorithms can be changed by setting a custom {@link Policy}.
*
* @param preferredHashAlgorithms list of preferred hash algorithms of a key
* @return first acceptable algorithm, or policies default hash algorithm
*/
private static HashAlgorithm negotiateHashAlgorithm(List<HashAlgorithm> preferredHashAlgorithms) {
if (preferredHashAlgorithms.isEmpty()) {
return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm();
Policy policy = PGPainless.getPolicy();
for (HashAlgorithm option : preferredHashAlgorithms) {
if (policy.getSignatureHashAlgorithmPolicy().isAcceptable(option)) {
return option;
}
}
return preferredHashAlgorithms.get(0);
return PGPainless.getPolicy().getSignatureHashAlgorithmPolicy().defaultHashAlgorithm();
}
public static PGPSignature getLatestValidSignature(PGPPublicKey publicKey, List<PGPSignature> signatures, PGPKeyRing keyRing) throws PGPException {
/**
* Return the latest valid signature on the provided public key.
*
* @param publicKey signed key
* @param signatures signatures
* @param keyRing key ring containing signature creator key
* @return latest valid signature
* @throws PGPException in case of a validation error
*/
public static PGPSignature getLatestValidSignature(PGPPublicKey publicKey,
List<PGPSignature> signatures,
PGPKeyRing keyRing)
throws PGPException {
List<PGPSignature> valid = new ArrayList<>();
for (PGPSignature signature : signatures) {
long issuerID = signature.getKeyID();
@ -93,6 +144,16 @@ public class SignatureUtils {
return valid.isEmpty() ? null : valid.get(valid.size() - 1);
}
/**
* Return true, iff a signature is valid.
*
* TODO: There is code duplication here ({@link SelectSignatureFromKey}, {@link SignatureChainValidator}, {@link SignatureValidator}).
* @param signature signature to validate
* @param issuer signing key
* @param target signed key
* @return true if signature is valid
* @throws PGPException if a validation error occurs.
*/
public static boolean isSignatureValid(PGPSignature signature, PGPPublicKey issuer, PGPPublicKey target) throws PGPException {
SignatureType signatureType = SignatureType.valueOf(signature.getSignatureType());
switch (signatureType) {
@ -276,6 +337,15 @@ public class SignatureUtils {
return latestSelfSig.getSignatureType() != SignatureType.CERTIFICATION_REVOCATION.getCode();
}
/**
* Return true if the provided signature is a hard revocation.
* Hard revocations are revocation signatures which either carry a revocation reason of
* {@link RevocationAttributes.Reason#KEY_COMPROMISED} or {@link RevocationAttributes.Reason#NO_REASON},
* or no reason at all.
*
* @param signature signature
* @return true if signature is a hard revocation
*/
public static boolean isHardRevocation(PGPSignature signature) {
SignatureType type = SignatureType.valueOf(signature.getSignatureType());

View file

@ -19,15 +19,30 @@ import java.util.Comparator;
import org.bouncycastle.openpgp.PGPSignature;
/**
* Comparator which sorts signatures based on an ordering and on revocation hardness.
*
* If a list of signatures gets ordered using this comparator, hard revocations will always
* come first.
* Further, signatures are ordered by date according to the {@link org.pgpainless.signature.SignatureCreationDateComparator.Order}.
*/
public class SignatureValidityComparator implements Comparator<PGPSignature> {
private final SignatureCreationDateComparator.Order order;
private final SignatureCreationDateComparator creationDateComparator;
/**
* Create a new {@link SignatureValidityComparator} which orders signatures oldest first.
* Still, hard revocations will come first.
*/
public SignatureValidityComparator() {
this(SignatureCreationDateComparator.DEFAULT_ORDER);
}
/**
* Create a new {@link SignatureValidityComparator} which orders signatures following the passed ordering.
* Still, hard revocations will come first.
*/
public SignatureValidityComparator(SignatureCreationDateComparator.Order order) {
this.order = order;
this.creationDateComparator = new SignatureCreationDateComparator(order);

View file

@ -31,6 +31,15 @@ import org.pgpainless.algorithm.KeyFlag;
*/
public class SignatureSubpacketGeneratorUtil {
/**
* Return a list of {@link SignatureSubpacket SignatureSubpackets} from the subpacket generator, which correspond
* to the given {@link org.pgpainless.algorithm.SignatureSubpacket} type.
*
* @param type subpacket type
* @param generator subpacket generator
* @param <P> generic subpacket type
* @return possibly empty list of subpackets
*/
public static <P extends SignatureSubpacket> List<P> getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket type,
PGPSignatureSubpacketGenerator generator) {
SignatureSubpacket[] subpackets = generator.getSubpackets(type.getCode());
@ -41,11 +50,25 @@ public class SignatureSubpacketGeneratorUtil {
return list;
}
/**
* Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators}
* internal set.
*
* @param subpacketType type of subpacket to remove
* @param subpacketGenerator subpacket generator
*/
public static void removeAllPacketsOfType(org.pgpainless.algorithm.SignatureSubpacket subpacketType,
PGPSignatureSubpacketGenerator subpacketGenerator) {
removeAllPacketsOfType(subpacketType.getCode(), subpacketGenerator);
}
/**
* Remove all packets of the given type from the {@link PGPSignatureSubpacketGenerator PGPSignatureSubpacketGenerators}
* internal set.
*
* @param type type of subpacket to remove
* @param subpacketGenerator subpacket generator
*/
public static void removeAllPacketsOfType(int type, PGPSignatureSubpacketGenerator subpacketGenerator) {
for (SignatureSubpacket subpacket : subpacketGenerator.getSubpackets(type)) {
subpacketGenerator.removePacket(subpacket);
@ -98,11 +121,22 @@ public class SignatureSubpacketGeneratorUtil {
return secondsToExpire;
}
/**
* Return true, if the subpacket generator has a {@link KeyFlags} subpacket which carries the given key flag.
* Returns false, if no {@link KeyFlags} subpacket is present.
* If there are more than one instance of a {@link KeyFlags} packet present, only the last occurrence will
* be tested.
*
* @param keyFlag flag to test for
* @param generator subpackets generator
* @return true if the generator has the given key flag set
*/
public static boolean hasKeyFlag(KeyFlag keyFlag, PGPSignatureSubpacketGenerator generator) {
List<KeyFlags> keyFlagPackets = getSubpacketsOfType(org.pgpainless.algorithm.SignatureSubpacket.keyFlags, generator);
if (keyFlagPackets.isEmpty()) {
return false;
}
return KeyFlag.hasKeyFlag(keyFlagPackets.get(0).getFlags(), keyFlag);
KeyFlags last = keyFlagPackets.get(keyFlagPackets.size() - 1);
return KeyFlag.hasKeyFlag(last.getFlags(), keyFlag);
}
}

View file

@ -46,6 +46,7 @@ import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
import org.bouncycastle.util.encoders.Hex;
import org.pgpainless.algorithm.SignatureSubpacket;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.signature.SignatureUtils;
/**
* Utility class to access signature subpackets from signatures.
@ -67,6 +68,13 @@ public class SignatureSubpacketsUtil {
return hashedOrUnhashed(signature, SignatureSubpacket.issuerFingerprint);
}
/**
* Return the {@link IssuerFingerprint} subpacket of the signature into a {@link OpenPgpV4Fingerprint}.
* If no issuer fingerprint is present in the signature, return null.
*
* @param signature signature
* @return v4 fingerprint of the issuer, or null
*/
public static OpenPgpV4Fingerprint getIssuerFingerprintAsOpenPgpV4Fingerprint(PGPSignature signature) {
IssuerFingerprint subpacket = getIssuerFingerprint(signature);
if (subpacket == null) {
@ -123,12 +131,20 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.signatureExpirationTime);
}
/**
* Return the signatures expiration time as a date.
* The expiration date is computed by adding the expiration time to the signature creation date.
* If the signature has no expiration time subpacket, or the expiration time is set to '0', this message returns null.
*
* @param signature signature
* @return expiration time as date
*/
public static Date getSignatureExpirationTimeAsDate(PGPSignature signature) {
SignatureExpirationTime subpacket = getSignatureExpirationTime(signature);
if (subpacket == null || subpacket.getTime() == 0) {
if (subpacket == null) {
return null;
}
return new Date(signature.getCreationTime().getTime() + 1000 * subpacket.getTime());
return SignatureUtils.datePlusSeconds(signature.getCreationTime(), subpacket.getTime());
}
/**
@ -142,15 +158,25 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.keyExpirationTime);
}
/**
* Return the signatures key-expiration time as a date.
* The expiration date is computed by adding the signatures' key-expiration time to the signing keys
* creation date.
* If the signature does not have a key-expiration time subpacket, or its value is '0', this method returns null.
*
* @param signature self-signature carrying the key-expiration time subpacket
* @param signingKey signature creation key
* @return key expiration time as date
*/
public static Date getKeyExpirationTimeAsDate(PGPSignature signature, PGPPublicKey signingKey) {
KeyExpirationTime subpacket = getKeyExpirationTime(signature);
if (subpacket == null || subpacket.getTime() == 0) {
if (subpacket == null) {
return null;
}
if (signature.getKeyID() != signingKey.getKeyID()) {
throw new IllegalArgumentException("Provided key (" + Long.toHexString(signingKey.getKeyID()) + ") did not create the signature (" + Long.toHexString(signature.getKeyID()) + ")");
}
return new Date(signingKey.getCreationTime().getTime() + 1000 * subpacket.getTime());
return SignatureUtils.datePlusSeconds(signingKey.getCreationTime(), subpacket.getTime());
}
/**
@ -328,14 +354,41 @@ public class SignatureSubpacketsUtil {
return hashed(signature, SignatureSubpacket.trustSignature);
}
/**
* Select a list of all signature subpackets of the given type, which are present in the hashed area of
* the given signature.
*
* @param signature signature
* @param type subpacket type
* @param <P> generic subpacket type
* @return list of subpackets from the hashed area
*/
private static <P extends org.bouncycastle.bcpg.SignatureSubpacket> P hashed(PGPSignature signature, SignatureSubpacket type) {
return getSignatureSubpacket(signature.getHashedSubPackets(), type);
}
/**
* Select a list of all signature subpackets of the given type, which are present in the unhashed area of
* the given signature.
*
* @param signature signature
* @param type subpacket type
* @param <P> generic subpacket type
* @return list of subpackets from the unhashed area
*/
private static <P extends org.bouncycastle.bcpg.SignatureSubpacket> P unhashed(PGPSignature signature, SignatureSubpacket type) {
return getSignatureSubpacket(signature.getUnhashedSubPackets(), type);
}
/**
* Select a list of all signature subpackets of the given type, which are present in either the hashed
* or the unhashed area of the given signature.
*
* @param signature signature
* @param type subpacket type
* @param <P> generic subpacket type
* @return list of subpackets from the hashed/unhashed area
*/
private static <P extends org.bouncycastle.bcpg.SignatureSubpacket> P hashedOrUnhashed(PGPSignature signature, SignatureSubpacket type) {
P hashedSubpacket = hashed(signature, type);
return hashedSubpacket != null ? hashedSubpacket : unhashed(signature, type);

View file

@ -15,7 +15,6 @@
*/
package org.pgpainless.util.selection.signature;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;