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:
parent
302e690b44
commit
8b9d41004b
5 changed files with 265 additions and 10 deletions
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -295,7 +295,8 @@ class MessageMetadata(val message: Message) {
|
|||
email,
|
||||
it.signature.creationTime,
|
||||
targetAmount)
|
||||
.authenticated
|
||||
?.authenticated
|
||||
?: false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue