1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-09-09 18:29:39 +02:00

Port CertificateAuthority to KeyIdentifier, add tests for authenticated cert selection

This commit is contained in:
Paul Schaub 2025-05-13 12:28:54 +02:00
parent 06d0b90ff6
commit 82db3a9ea6
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
5 changed files with 265 additions and 10 deletions

View file

@ -5,14 +5,46 @@
package org.pgpainless.authentication
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.api.OpenPGPCertificate
import org.pgpainless.PGPainless
/**
* A certificate authenticity record, indicating, to what degree the certificate is authenticated.
*
* @param userId identity, was changed to [CharSequence] instead of [String] starting with
* PGPainless 2.0.
* @param certificate certificate, was changed to [OpenPGPCertificate] instead of
* [PGPPublicKeyRing]. Use [pgpPublicKeyRing] if you need to access a [PGPPublicKeyRing].
* @param certificationChains map of chains and their trust degrees
* @param targetAmount targeted trust amount
*/
class CertificateAuthenticity(
val userId: String,
val certificate: PGPPublicKeyRing,
val userId: CharSequence,
val certificate: OpenPGPCertificate,
val certificationChains: Map<CertificationChain, Int>,
val targetAmount: Int
) {
/** Legacy constructor accepting a [PGPPublicKeyRing]. */
@Deprecated("Pass in an OpenPGPCertificate instead of a PGPPublicKeyRing.")
constructor(
userId: String,
certificate: PGPPublicKeyRing,
certificationChains: Map<CertificationChain, Int>,
targetAmount: Int
) : this(
userId,
PGPainless.getInstance().toCertificate(certificate),
certificationChains,
targetAmount)
/**
* Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for
* [certificate].
*/
@Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate"))
val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing
val totalTrustAmount: Int
get() = certificationChains.values.sum()
@ -44,5 +76,22 @@ class CertificateAuthenticity(
*/
class CertificationChain(val trustAmount: Int, val chainLinks: List<ChainLink>) {}
/** A chain link contains a node in the trust chain. */
class ChainLink(val certificate: PGPPublicKeyRing) {}
/**
* A chain link contains a node in the trust chain.
*
* @param certificate chain link certificate, was changed from [PGPPublicKeyRing] to
* [OpenPGPCertificate] with PGPainless 2.0. Use [pgpPublicKeyRing] if you need to access the
* field as [PGPPublicKeyRing].
*/
class ChainLink(val certificate: OpenPGPCertificate) {
constructor(
certificate: PGPPublicKeyRing
) : this(PGPainless.getInstance().toCertificate(certificate))
/**
* Field was introduced to allow backwards compatibility with pre-2.0 API as replacement for
* [certificate].
*/
@Deprecated("Use certificate instead.", replaceWith = ReplaceWith("certificate"))
val pgpPublicKeyRing: PGPPublicKeyRing = certificate.pgpPublicKeyRing
}

View file

@ -5,6 +5,7 @@
package org.pgpainless.authentication
import java.util.*
import org.bouncycastle.bcpg.KeyIdentifier
import org.pgpainless.key.OpenPgpFingerprint
/**
@ -36,7 +37,30 @@ interface CertificateAuthority {
email: Boolean,
referenceTime: Date,
targetAmount: Int
): CertificateAuthenticity
): CertificateAuthenticity? =
authenticateBinding(fingerprint.keyIdentifier, userId, email, referenceTime, targetAmount)
/**
* Determine the authenticity of the binding between the given cert identifier and the userId.
* In other words, determine, how much evidence can be gathered, that the certificate with the
* given fingerprint really belongs to the user with the given userId.
*
* @param certIdentifier identifier of the certificate
* @param userId userId
* @param email if true, the userId will be treated as an email address and all user-IDs
* containing the email address will be matched.
* @param referenceTime reference time at which the binding shall be evaluated
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly
* authenticated, 60 = partially authenticated...)
* @return information about the authenticity of the binding
*/
fun authenticateBinding(
certIdentifier: KeyIdentifier,
userId: CharSequence,
email: Boolean,
referenceTime: Date,
targetAmount: Int
): CertificateAuthenticity?
/**
* Lookup certificates, which carry a trustworthy binding to the given userId.
@ -50,7 +74,7 @@ interface CertificateAuthority {
* @return list of identified bindings
*/
fun lookupByUserId(
userId: String,
userId: CharSequence,
email: Boolean,
referenceTime: Date,
targetAmount: Int
@ -70,5 +94,22 @@ interface CertificateAuthority {
fingerprint: OpenPgpFingerprint,
referenceTime: Date,
targetAmount: Int
): List<CertificateAuthenticity> =
identifyByFingerprint(fingerprint.keyIdentifier, referenceTime, targetAmount)
/**
* Identify trustworthy bindings for a certificate. The result is a list of authenticatable
* userIds on the certificate.
*
* @param certIdentifier identifier of the certificate
* @param referenceTime reference time for trust calculations
* @param targetAmount target trust amount (120 = fully authenticated, 240 = doubly
* authenticated, 60 = partially authenticated...)
* @return list of identified bindings
*/
fun identifyByFingerprint(
certIdentifier: KeyIdentifier,
referenceTime: Date,
targetAmount: Int
): List<CertificateAuthenticity>
}

View file

@ -295,7 +295,8 @@ class MessageMetadata(val message: Message) {
email,
it.signature.creationTime,
targetAmount)
.authenticated
?.authenticated
?: false
}
}

View file

@ -90,9 +90,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api:
authority
.lookupByUserId(userId, email, evaluationDate, targetAmount)
.filter { it.isAuthenticated() }
.forEach {
addRecipient(api.toCertificate(it.certificate)).also { foundAcceptable = true }
}
.forEach { addRecipient(it.certificate).also { foundAcceptable = true } }
require(foundAcceptable) {
"Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount."
}

View file

@ -0,0 +1,166 @@
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import org.bouncycastle.bcpg.KeyIdentifier;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.api.OpenPGPCertificate;
import org.bouncycastle.openpgp.api.OpenPGPKey;
import org.bouncycastle.util.io.Streams;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.TestTemplate;
import org.junit.jupiter.api.extension.ExtendWith;
import org.pgpainless.PGPainless;
import org.pgpainless.authentication.CertificateAuthenticity;
import org.pgpainless.authentication.CertificateAuthority;
import org.pgpainless.authentication.CertificationChain;
import org.pgpainless.authentication.ChainLink;
import org.pgpainless.encryption_signing.EncryptionOptions;
import org.pgpainless.encryption_signing.EncryptionStream;
import org.pgpainless.encryption_signing.ProducerOptions;
import org.pgpainless.encryption_signing.SigningOptions;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.util.TestAllImplementations;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class VerifyWithCertificateAuthorityTest {
@TestTemplate
@ExtendWith(TestAllImplementations.class)
public void testVerifySignatureFromAuthenticatedCert() throws PGPException, IOException {
PGPainless api = PGPainless.getInstance();
OpenPGPKey aliceKey = api.generateKey().modernKeyRing("Alice <alice@pgpainless.org>");
OpenPGPCertificate aliceCert = aliceKey.toCertificate();
SimpleCertificateAuthority authority = new SimpleCertificateAuthority();
authority.addDirectlyAuthenticatedCert(aliceCert, 120);
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
EncryptionStream eOut = api.generateMessage()
.onOutputStream(bOut)
.withOptions(ProducerOptions.signAndEncrypt(
EncryptionOptions.encryptCommunications()
.addAuthenticatableRecipients("Alice <alice@pgpainless.org>", false, authority),
SigningOptions.get().addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), aliceKey)
));
eOut.write("Hello, World!\n".getBytes(StandardCharsets.UTF_8));
eOut.close();
ByteArrayInputStream bIn = new ByteArrayInputStream(bOut.toByteArray());
DecryptionStream dIn = api.processMessage()
.onInputStream(bIn)
.withOptions(ConsumerOptions.get()
.addVerificationCert(aliceCert)
.addDecryptionKey(aliceKey));
Streams.drain(dIn);
dIn.close();
MessageMetadata metadata = dIn.getMetadata();
assertTrue(metadata.isEncryptedFor(aliceCert));
assertTrue(metadata.isAuthenticatablySignedBy("Alice <alice@pgpainless.org>", false, authority));
assertTrue(metadata.isAuthenticatablySignedBy("alice@pgpainless.org", true, authority));
assertFalse(metadata.isAuthenticatablySignedBy("mallory@pgpainless.org", true, authority));
}
public static class SimpleCertificateAuthority implements CertificateAuthority {
Map<OpenPGPCertificate, Integer> directlyAuthenticatedCerts = new HashMap<>();
public void addDirectlyAuthenticatedCert(OpenPGPCertificate cert, int trustAmount) {
directlyAuthenticatedCerts.put(cert, trustAmount);
}
@Override
public CertificateAuthenticity authenticateBinding(
@NotNull KeyIdentifier certIdentifier,
@NotNull CharSequence userId,
boolean email,
@NotNull Date referenceTime,
int targetAmount) {
Optional<OpenPGPCertificate> opt = directlyAuthenticatedCerts.keySet().stream()
.filter(it -> it.getKey(certIdentifier) != null)
.findFirst();
if (opt.isEmpty()) {
return null;
}
OpenPGPCertificate cert = opt.get();
Optional<OpenPGPCertificate.OpenPGPUserId> uid;
if (email) {
uid = cert.getAllUserIds().stream().filter(it -> it.getUserId().contains("<" + userId + ">"))
.findFirst();
} else {
uid = cert.getAllUserIds().stream().filter(it -> it.getUserId().contentEquals(userId))
.findFirst();
}
return uid.map(openPGPUserId -> authenticatedUserId(openPGPUserId, targetAmount)).orElse(null);
}
@NotNull
@Override
public List<CertificateAuthenticity> lookupByUserId(
@NotNull CharSequence userId,
boolean email,
@NotNull Date referenceTime,
int targetAmount) {
List<CertificateAuthenticity> matches = new ArrayList<>();
for (OpenPGPCertificate cert : directlyAuthenticatedCerts.keySet()) {
cert.getAllUserIds()
.stream().filter(it -> {
if (email) return it.getUserId().contains("<" + userId + ">");
else return it.getUserId().contentEquals(userId);
}).forEach(it -> {
matches.add(authenticatedUserId(it, targetAmount));
});
}
return matches;
}
@NotNull
@Override
public List<CertificateAuthenticity> identifyByFingerprint(
@NotNull KeyIdentifier certIdentifier,
@NotNull Date referenceTime,
int targetAmount) {
List<CertificateAuthenticity> matches = new ArrayList<>();
directlyAuthenticatedCerts.keySet()
.stream().filter(it -> it.getKey(certIdentifier) != null)
.forEach(it -> {
for (OpenPGPCertificate.OpenPGPUserId userId : it.getAllUserIds()) {
matches.add(authenticatedUserId(userId, targetAmount));
}
});
return matches;
}
private CertificateAuthenticity authenticatedUserId(OpenPGPCertificate.OpenPGPUserId userId, int targetAmount) {
OpenPGPCertificate cert = userId.getCertificate();
int certTrust = directlyAuthenticatedCerts.get(cert);
Map<CertificationChain, Integer> chains = new HashMap<>();
chains.put(new CertificationChain(certTrust, Collections.singletonList(new ChainLink(cert))), certTrust);
return new CertificateAuthenticity(userId.getUserId(), cert, chains, targetAmount);
}
}
}