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
06d0b90ff6
commit
82db3a9ea6
5 changed files with 265 additions and 10 deletions
|
@ -5,14 +5,46 @@
|
||||||
package org.pgpainless.authentication
|
package org.pgpainless.authentication
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKeyRing
|
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(
|
class CertificateAuthenticity(
|
||||||
val userId: String,
|
val userId: CharSequence,
|
||||||
val certificate: PGPPublicKeyRing,
|
val certificate: OpenPGPCertificate,
|
||||||
val certificationChains: Map<CertificationChain, Int>,
|
val certificationChains: Map<CertificationChain, Int>,
|
||||||
val targetAmount: 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
|
val totalTrustAmount: Int
|
||||||
get() = certificationChains.values.sum()
|
get() = certificationChains.values.sum()
|
||||||
|
|
||||||
|
@ -44,5 +76,22 @@ class CertificateAuthenticity(
|
||||||
*/
|
*/
|
||||||
class CertificationChain(val trustAmount: Int, val chainLinks: List<ChainLink>) {}
|
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
|
package org.pgpainless.authentication
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import org.bouncycastle.bcpg.KeyIdentifier
|
||||||
import org.pgpainless.key.OpenPgpFingerprint
|
import org.pgpainless.key.OpenPgpFingerprint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,7 +37,30 @@ interface CertificateAuthority {
|
||||||
email: Boolean,
|
email: Boolean,
|
||||||
referenceTime: Date,
|
referenceTime: Date,
|
||||||
targetAmount: Int
|
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.
|
* Lookup certificates, which carry a trustworthy binding to the given userId.
|
||||||
|
@ -50,7 +74,7 @@ interface CertificateAuthority {
|
||||||
* @return list of identified bindings
|
* @return list of identified bindings
|
||||||
*/
|
*/
|
||||||
fun lookupByUserId(
|
fun lookupByUserId(
|
||||||
userId: String,
|
userId: CharSequence,
|
||||||
email: Boolean,
|
email: Boolean,
|
||||||
referenceTime: Date,
|
referenceTime: Date,
|
||||||
targetAmount: Int
|
targetAmount: Int
|
||||||
|
@ -70,5 +94,22 @@ interface CertificateAuthority {
|
||||||
fingerprint: OpenPgpFingerprint,
|
fingerprint: OpenPgpFingerprint,
|
||||||
referenceTime: Date,
|
referenceTime: Date,
|
||||||
targetAmount: Int
|
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>
|
): List<CertificateAuthenticity>
|
||||||
}
|
}
|
||||||
|
|
|
@ -295,7 +295,8 @@ class MessageMetadata(val message: Message) {
|
||||||
email,
|
email,
|
||||||
it.signature.creationTime,
|
it.signature.creationTime,
|
||||||
targetAmount)
|
targetAmount)
|
||||||
.authenticated
|
?.authenticated
|
||||||
|
?: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -90,9 +90,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose, private val api:
|
||||||
authority
|
authority
|
||||||
.lookupByUserId(userId, email, evaluationDate, targetAmount)
|
.lookupByUserId(userId, email, evaluationDate, targetAmount)
|
||||||
.filter { it.isAuthenticated() }
|
.filter { it.isAuthenticated() }
|
||||||
.forEach {
|
.forEach { addRecipient(it.certificate).also { foundAcceptable = true } }
|
||||||
addRecipient(api.toCertificate(it.certificate)).also { foundAcceptable = true }
|
|
||||||
}
|
|
||||||
require(foundAcceptable) {
|
require(foundAcceptable) {
|
||||||
"Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount."
|
"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