1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-11-05 03:55:58 +01:00

Refactor WebOfTrust class

This commit is contained in:
Paul Schaub 2023-07-07 14:22:45 +02:00
parent f13f310f6b
commit 1b19ba8766
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
5 changed files with 193 additions and 267 deletions

View file

@ -18,7 +18,6 @@ import org.pgpainless.signature.SignatureUtils
import org.pgpainless.signature.consumer.SignatureVerifier import org.pgpainless.signature.consumer.SignatureVerifier
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
import org.pgpainless.wot.dijkstra.sq.* import org.pgpainless.wot.dijkstra.sq.*
import org.pgpainless.wot.dijkstra.sq.CertificationSet.Companion.fromCertification
import org.pgpainless.wot.dijkstra.sq.ReferenceTime.Companion.now import org.pgpainless.wot.dijkstra.sq.ReferenceTime.Companion.now
import org.pgpainless.wot.util.CertificationFactory.Companion.fromCertification import org.pgpainless.wot.util.CertificationFactory.Companion.fromCertification
import org.pgpainless.wot.util.CertificationFactory.Companion.fromDelegation import org.pgpainless.wot.util.CertificationFactory.Companion.fromDelegation
@ -46,11 +45,10 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) {
*/ */
constructor(certificateDirectory: PGPCertificateDirectory): this(PGPCertificateStoreAdapter(certificateDirectory)) constructor(certificateDirectory: PGPCertificateDirectory): this(PGPCertificateStoreAdapter(certificateDirectory))
lateinit var network: Network fun buildNetwork(policy: Policy = PGPainless.getPolicy(), referenceTime: ReferenceTime = now()): Network {
fun initialize() {
val certificates = getAllCertificatesFromTheStore() val certificates = getAllCertificatesFromTheStore()
network = fromCertificates(certificates, PGPainless.getPolicy(), now()) val networkFactory = PGPNetworkFactory.fromCertificates(certificates, policy, referenceTime)
return networkFactory.buildNetwork()
} }
private fun getAllCertificatesFromTheStore(): Sequence<Certificate> { private fun getAllCertificatesFromTheStore(): Sequence<Certificate> {
@ -69,47 +67,181 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) {
return certificates return certificates
} }
companion object { /**
@JvmStatic * Class for building the [Flow network][Network] from the given set of OpenPGP keys.
fun fromCertificates(certificates: Sequence<Certificate>, */
policy: Policy, private class PGPNetworkFactory private constructor(validatedCertificates: List<KeyRingInfo>,
referenceTime: ReferenceTime): Network { private val policy: Policy,
return fromValidCertificates( private val referenceTime: ReferenceTime) {
parseValidCertificates(certificates, policy, referenceTime),
policy,
referenceTime
)
}
@JvmStatic companion object {
fun fromValidCertificates(certificates: List<KeyRingInfo>, @JvmStatic
policy: Policy, private val LOGGER = LoggerFactory.getLogger(PGPNetworkFactory::class.java)
referenceTime: ReferenceTime): Network {
val nb = NetworkBuilder(certificates, policy, referenceTime)
return nb.buildNetwork()
}
@JvmStatic @JvmStatic
private fun parseValidCertificates(certificates: Sequence<Certificate>, fun fromCertificates(certificates: Sequence<Certificate>,
policy: Policy, policy: Policy,
referenceTime: ReferenceTime): List<KeyRingInfo> { referenceTime: ReferenceTime): PGPNetworkFactory {
return certificates return fromValidCertificates(
.mapNotNull { cert -> parseValidCertificates(certificates, policy, referenceTime),
try { policy,
PGPainless.readKeyRing().publicKeyRing(cert.inputStream) referenceTime
} catch (e: IOException) { )
null }
@JvmStatic
fun fromValidCertificates(certificates: List<KeyRingInfo>,
policy: Policy,
referenceTime: ReferenceTime): PGPNetworkFactory {
return PGPNetworkFactory(certificates, policy, referenceTime)
}
@JvmStatic
private fun parseValidCertificates(certificates: Sequence<Certificate>,
policy: Policy,
referenceTime: ReferenceTime): List<KeyRingInfo> {
return certificates
.mapNotNull {
try { PGPainless.readKeyRing().publicKeyRing(it.inputStream) } catch (e: IOException) { null }
} }
} .map { KeyRingInfo(it, policy, referenceTime.timestamp) }
.map { cert -> .toList()
KeyRingInfo(cert, policy, referenceTime.timestamp) }
}
.toList()
} }
// Map signature to its revocation state private val networkBuilder: Network.Builder = Network.builder()
@JvmStatic
private fun revocationStateFromSignature(revocation: PGPSignature?): RevocationState { // certificates keyed by fingerprint
private val byFingerprint: MutableMap<Fingerprint, KeyRingInfo> = HashMap()
// certificates keyed by (sub-) key-id
private val byKeyId: MutableMap<Long, MutableList<KeyRingInfo>> = HashMap()
// certificate synopses keyed by fingerprint
private val certSynopsisMap: MutableMap<Fingerprint, CertSynopsis> = HashMap()
init {
validatedCertificates.forEach { indexAsNode(it) }
validatedCertificates.forEach { findEdgesWithTarget(it) }
}
private fun indexAsNode(cert: KeyRingInfo) {
// index by fingerprint
val certFingerprint = Fingerprint(cert.fingerprint)
if (!byFingerprint.containsKey(certFingerprint)) {
byFingerprint[certFingerprint] = cert
}
// index by key-ID
var certsWithKey = byKeyId[cert.keyId]
// noinspection Java8MapApi
if (certsWithKey == null) {
certsWithKey = mutableListOf()
// TODO: Something is fishy here...
for (key in cert.validSubkeys) {
byKeyId[key.keyID] = certsWithKey
}
}
certsWithKey.add(cert)
val userIds: MutableMap<String, RevocationState> = HashMap()
for (userId in cert.userIds) {
val state = RevocationState(cert.getUserIdRevocation(userId))
userIds[userId] = state
}
// index synopses
val expirationDate: Date? = try {
cert.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER)
} catch (e: NoSuchElementException) {
// Some keys are malformed and have no KeyFlags
return
}
val node = CertSynopsis(certFingerprint,
expirationDate,
RevocationState(cert.revocationSelfSignature),
userIds)
certSynopsisMap[certFingerprint] = node
networkBuilder.addNode(node)
}
private fun findEdgesWithTarget(validatedTarget: KeyRingInfo) {
val validatedTargetKeyRing = KeyRingUtils.publicKeys(validatedTarget.keys)
val targetFingerprint = Fingerprint(OpenPgpFingerprint.of(validatedTargetKeyRing))
val targetPrimaryKey = validatedTargetKeyRing.publicKey!!
val target = certSynopsisMap[targetFingerprint]!!
// Direct-Key Signatures (delegations) by X on Y
val delegations = SignatureUtils.getDelegations(validatedTargetKeyRing)
for (delegation in delegations) {
processDelegation(targetPrimaryKey, target, delegation)
}
// Certification Signatures by X on Y over user-ID U
val userIds = targetPrimaryKey.userIDs
while (userIds.hasNext()) {
val userId = userIds.next()
val userIdSigs = SignatureUtils.get3rdPartyCertificationsFor(userId, validatedTargetKeyRing)
userIdSigs.forEach {
processCertificationOnUserId(targetPrimaryKey, target, userId, it)
}
}
}
private fun processDelegation(targetPrimaryKey: PGPPublicKey,
target: CertSynopsis,
delegation: PGPSignature) {
val issuerCandidates = byKeyId[delegation.keyID]
?: return
for (candidate in issuerCandidates) {
val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys)
val issuerFingerprint = Fingerprint(OpenPgpFingerprint.of(issuerKeyRing))
val issuerSigningKey = issuerKeyRing.getPublicKey(delegation.keyID)
val issuer = certSynopsisMap[issuerFingerprint]
?: continue
try {
val valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey,
targetPrimaryKey, policy, referenceTime.timestamp)
if (valid) {
networkBuilder.addEdge(fromDelegation(issuer, target, delegation))
}
} catch (e: SignatureValidationException) {
val targetFingerprint = OpenPgpFingerprint.of(targetPrimaryKey)
LOGGER.warn("Cannot verify signature by $issuerFingerprint on cert of $targetFingerprint", e)
}
}
}
private fun processCertificationOnUserId(targetPrimaryKey: PGPPublicKey,
target: CertSynopsis,
userId: String,
certification: PGPSignature) {
val issuerCandidates = byKeyId[certification.keyID]
?: return
for (candidate in issuerCandidates) {
val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys)
val issuerFingerprint = Fingerprint(OpenPgpFingerprint.of(issuerKeyRing))
val issuerSigningKey = issuerKeyRing.getPublicKey(certification.keyID)
?: continue
val issuer = certSynopsisMap[issuerFingerprint]
?: continue
try {
val valid = SignatureVerifier.verifySignatureOverUserId(userId, certification,
issuerSigningKey, targetPrimaryKey, policy, referenceTime.timestamp)
if (valid) {
networkBuilder.addEdge(fromCertification(issuer, target, userId, certification))
}
} catch (e: SignatureValidationException) {
LOGGER.warn("Cannot verify signature for '$userId' by $issuerFingerprint" +
" on cert of ${target.fingerprint}", e)
}
}
}
private fun Fingerprint(fingerprint: OpenPgpFingerprint) = Fingerprint(fingerprint.toString())
private fun RevocationState(revocation: PGPSignature?): RevocationState {
if (revocation == null) { if (revocation == null) {
return RevocationState.notRevoked() return RevocationState.notRevoked()
} }
@ -121,209 +253,13 @@ class WebOfTrust(private val certificateStore: PGPCertificateStore) {
RevocationState.softRevoked(revocation.creationTime) RevocationState.softRevoked(revocation.creationTime)
} }
@JvmStatic
private fun OpenPgpFingerprint.map(): Fingerprint {
return Fingerprint(toString())
}
/** /**
* Class for building the [Flow network][Network] from the given set of OpenPGP keys. * Return the constructed, initialized [Network].
* *
* @return finished network
*/ */
private class NetworkBuilder constructor(validatedCertificates: List<KeyRingInfo>, fun buildNetwork(): Network {
private val policy: Policy, return networkBuilder.build()
private val referenceTime: ReferenceTime) {
companion object {
@JvmStatic
private val LOGGER = LoggerFactory.getLogger(NetworkBuilder::class.java)
}
// certificates keyed by fingerprint
private val byFingerprint: MutableMap<Fingerprint, KeyRingInfo> = HashMap()
// certificates keyed by (sub-) key-id
private val byKeyId: MutableMap<Long, MutableList<KeyRingInfo>> = HashMap()
// certificate synopses keyed by fingerprint
private val certSynopsisMap: MutableMap<Fingerprint, CertSynopsis> = HashMap()
// Issuer -> Targets, edges keyed by issuer
private val edges: MutableMap<Fingerprint, MutableList<CertificationSet>> = HashMap()
// Target -> Issuers, edges keyed by target
private val reverseEdges: MutableMap<Fingerprint, MutableList<CertificationSet>> = HashMap()
init {
synopsizeCertificates(validatedCertificates)
findEdges(validatedCertificates)
}
private fun synopsizeCertificates(validatedCertificates: List<KeyRingInfo>) {
for (cert in validatedCertificates) {
synopsize(cert)
}
}
private fun synopsize(cert: KeyRingInfo) {
// index by fingerprint
val certFingerprint = cert.fingerprint.map()
if (!byFingerprint.containsKey(certFingerprint)) {
byFingerprint[certFingerprint] = cert
}
// index by key-ID
var certsWithKey = byKeyId[cert.keyId]
// noinspection Java8MapApi
if (certsWithKey == null) {
certsWithKey = mutableListOf()
// TODO: Something is fishy here...
for (key in cert.validSubkeys) {
byKeyId[key.keyID] = certsWithKey
}
}
certsWithKey.add(cert)
val userIds: MutableMap<String, RevocationState> = HashMap()
for (userId in cert.userIds) {
val state: RevocationState = revocationStateFromSignature(cert.getUserIdRevocation(userId))
userIds[userId] = state
}
// index synopses
val expirationDate: Date? = try {
cert.getExpirationDateForUse(KeyFlag.CERTIFY_OTHER)
} catch (e: NoSuchElementException) {
// Some keys are malformed and have no KeyFlags
return
}
certSynopsisMap[certFingerprint] = CertSynopsis(certFingerprint,
expirationDate,
revocationStateFromSignature(cert.revocationSelfSignature),
userIds)
}
private fun findEdges(validatedCertificates: List<KeyRingInfo>) {
// Identify certifications and delegations
// Target = cert carrying a signature
for (validatedTarget in validatedCertificates) {
findEdgesWithTarget(validatedTarget)
}
}
private fun findEdgesWithTarget(validatedTarget: KeyRingInfo) {
val validatedTargetKeyRing = KeyRingUtils.publicKeys(validatedTarget.keys)
val targetFingerprint = OpenPgpFingerprint.of(validatedTargetKeyRing).map()
val targetPrimaryKey = validatedTargetKeyRing.publicKey!!
val target = certSynopsisMap[targetFingerprint]!!
// Direct-Key Signatures (delegations) by X on Y
val delegations = SignatureUtils.getDelegations(validatedTargetKeyRing)
for (delegation in delegations) {
processDelegation(targetPrimaryKey, target, delegation)
}
// Certification Signatures by X on Y over user-ID U
val userIds = targetPrimaryKey.userIDs
while (userIds.hasNext()) {
val userId = userIds.next()
val userIdSigs = SignatureUtils.get3rdPartyCertificationsFor(userId, validatedTargetKeyRing)
processCertification(targetPrimaryKey, target, userId, userIdSigs)
}
}
private fun processDelegation(targetPrimaryKey: PGPPublicKey,
target: CertSynopsis,
delegation: PGPSignature) {
val issuerCandidates = byKeyId[delegation.keyID]
?: return
for (candidate in issuerCandidates) {
val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys)
val issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing).map()
val issuerSigningKey = issuerKeyRing.getPublicKey(delegation.keyID)
val issuer = certSynopsisMap[issuerFingerprint]
?: continue
try {
val valid = SignatureVerifier.verifyDirectKeySignature(delegation, issuerSigningKey,
targetPrimaryKey, policy, referenceTime.timestamp)
if (valid) {
indexEdge(fromDelegation(issuer, target, delegation))
}
} catch (e: SignatureValidationException) {
val targetFingerprint = OpenPgpFingerprint.of(targetPrimaryKey)
LOGGER.warn("Cannot verify signature by $issuerFingerprint on cert of $targetFingerprint", e)
}
}
}
private fun processCertification(targetPrimaryKey: PGPPublicKey,
target: CertSynopsis,
userId: String,
userIdSigs: List<PGPSignature>) {
for (certification in userIdSigs) {
val issuerCandidates = byKeyId[certification.keyID]
?: continue
for (candidate in issuerCandidates) {
val issuerKeyRing = KeyRingUtils.publicKeys(candidate.keys)
val issuerFingerprint = OpenPgpFingerprint.of(issuerKeyRing).map()
val issuerSigningKey = issuerKeyRing.getPublicKey(certification.keyID)
?: continue
val issuer = certSynopsisMap[issuerFingerprint]
?: continue
try {
val valid = SignatureVerifier.verifySignatureOverUserId(userId, certification,
issuerSigningKey, targetPrimaryKey, policy, referenceTime.timestamp)
if (valid) {
indexEdge(fromCertification(issuer, target, userId, certification))
}
} catch (e: SignatureValidationException) {
LOGGER.warn("Cannot verify signature for '$userId' by $issuerFingerprint" +
" on cert of ${target.fingerprint}", e)
}
}
}
}
private fun indexEdge(certification: Certification) {
// Index edge as outgoing edge for issuer
val issuer = certification.issuer.fingerprint
edges.getOrPut(issuer) { mutableListOf() }.also { indexOutEdge(it, certification) }
// Index edge as incoming edge for target
val target = certification.target.fingerprint
reverseEdges.getOrPut(target) { mutableListOf() }.also { indexInEdge(it, certification) }
}
private fun indexOutEdge(outEdges: MutableList<CertificationSet>, certification: Certification) {
val target = certification.target.fingerprint
for (outEdge in outEdges) {
if (target == outEdge.target.fingerprint) {
outEdge.add(certification)
return
}
}
outEdges.add(fromCertification(certification))
}
private fun indexInEdge(inEdges: MutableList<CertificationSet>, certification: Certification) {
val issuer = certification.issuer.fingerprint
for (inEdge in inEdges) {
if (issuer == inEdge.issuer.fingerprint) {
inEdge.add(certification)
return
}
}
inEdges.add(fromCertification(certification))
}
/**
* Return the constructed, initialized [Network].
*
* @return finished network
*/
fun buildNetwork(): Network {
return Network(certSynopsisMap, edges, reverseEdges, referenceTime)
}
} }
} }
} }

View file

@ -8,7 +8,6 @@ class AdHocTest {
@Test @Test
fun test() { fun test() {
val store = AdHocVectors.BestViaRoot().pgpCertificateStore val store = AdHocVectors.BestViaRoot().pgpCertificateStore
val wot = WebOfTrust(store).also { it.initialize() } val network = WebOfTrust(store).buildNetwork()
val network = wot.network
} }
} }

View file

@ -33,9 +33,7 @@ class WebOfTrustTest {
@Test @Test
fun testWithTwoNodesAndOneDelegation() { fun testWithTwoNodesAndOneDelegation() {
val certD = TestCertificateStores.oneDelegationGraph() val certD = TestCertificateStores.oneDelegationGraph()
val wot = WebOfTrust(certD) val network = WebOfTrust(certD).buildNetwork()
wot.initialize()
val network = wot.network
assertEquals(2, network.nodes.size) assertEquals(2, network.nodes.size)
assertHasEdge(network, fooBankAdmin, barBankCa) assertHasEdge(network, fooBankAdmin, barBankCa)
@ -48,9 +46,7 @@ class WebOfTrustTest {
@Test @Test
fun testWithCrossSignedCertificates() { fun testWithCrossSignedCertificates() {
val certD = TestCertificateStores.disconnectedGraph() val certD = TestCertificateStores.disconnectedGraph()
val wot = WebOfTrust(certD) val network = WebOfTrust(certD).buildNetwork()
wot.initialize()
val network = wot.network
assertEquals(5, network.nodes.size) assertEquals(5, network.nodes.size)
assertTrue { assertTrue {
@ -81,9 +77,7 @@ class WebOfTrustTest {
@Test @Test
fun testWotCreationOfEmptyCertificates() { fun testWotCreationOfEmptyCertificates() {
val certD = TestCertificateStores.emptyGraph() val certD = TestCertificateStores.emptyGraph()
val wot = WebOfTrust(certD) val network = WebOfTrust(certD).buildNetwork()
wot.initialize()
val network = wot.network
assertTrue { network.nodes.isEmpty() } assertTrue { network.nodes.isEmpty() }
assertTrue { network.edges.isEmpty() } assertTrue { network.edges.isEmpty() }
@ -93,9 +87,7 @@ class WebOfTrustTest {
@Test @Test
fun testWotWithAnomaly() { fun testWotWithAnomaly() {
val store = TestCertificateStores.anomalyGraph() val store = TestCertificateStores.anomalyGraph()
val wot = WebOfTrust(store) val network = WebOfTrust(store).buildNetwork()
wot.initialize()
val network = wot.network
assertEquals(1, network.nodes.size) assertEquals(1, network.nodes.size)
} }

View file

@ -9,14 +9,13 @@ import org.junit.jupiter.api.assertThrows
import org.pgpainless.wot.dijkstra.sq.* import org.pgpainless.wot.dijkstra.sq.*
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
class CertificationSetTest { class CertificationSetTest {
private val alice = CertSynopsis(Fingerprint("0000000000000000000000000000000000000000"), null, RevocationState.notRevoked(), mapOf()) private val alice = CertSynopsis(Fingerprint("A"), null, RevocationState.notRevoked(), mapOf())
private val bob = CertSynopsis(Fingerprint("1111111111111111111111111111111111111111"), null, RevocationState.notRevoked(), mapOf()) private val bob = CertSynopsis(Fingerprint("B"), null, RevocationState.notRevoked(), mapOf())
private val charlie = CertSynopsis(Fingerprint("2222222222222222222222222222222222222222"), null, RevocationState.notRevoked(), mapOf()) private val charlie = CertSynopsis(Fingerprint("C"), null, RevocationState.notRevoked(), mapOf())
private val aliceSignsBob = Certification(alice, null, bob, Date()) private val aliceSignsBob = Certification(alice, null, bob, Date())
private val aliceSignsBobUserId = Certification(alice, "Bob <bob@example.org>", bob, Date()) private val aliceSignsBobUserId = Certification(alice, "Bob <bob@example.org>", bob, Date())
@ -105,8 +104,8 @@ class CertificationSetTest {
val twoCerts = CertificationSet.fromCertification(aliceSignsBob) val twoCerts = CertificationSet.fromCertification(aliceSignsBob)
twoCerts.add(aliceSignsBobUserId) twoCerts.add(aliceSignsBobUserId)
assertEquals("0000000000000000000000000000000000000000 delegates to 1111111111111111111111111111111111111111\n" + assertEquals("A certifies binding: null <-> B [120]\n" +
"0000000000000000000000000000000000000000 certifies [Bob <bob@example.org>] 1111111111111111111111111111111111111111", twoCerts.toString()) "A certifies binding: Bob <bob@example.org> <-> B [120]", twoCerts.toString())
} }
@Test @Test

View file

@ -11,17 +11,17 @@ import kotlin.test.assertEquals
class CertificationTest { class CertificationTest {
private val alice = CertSynopsis( private val alice = CertSynopsis(
Fingerprint("0000000000000000000000000000000000000000"), Fingerprint("A"),
null, null,
RevocationState.notRevoked(), RevocationState.notRevoked(),
mapOf(Pair("Alice <alice@pgpainless.org>", RevocationState.notRevoked()))) mapOf(Pair("Alice <alice@pgpainless.org>", RevocationState.notRevoked())))
private val bob = CertSynopsis( private val bob = CertSynopsis(
Fingerprint("1111111111111111111111111111111111111111"), Fingerprint("B"),
null, null,
RevocationState.notRevoked(), RevocationState.notRevoked(),
mapOf(Pair("Bob <bob@example.org>", RevocationState.notRevoked()))) mapOf(Pair("Bob <bob@example.org>", RevocationState.notRevoked())))
private val charlie = CertSynopsis( private val charlie = CertSynopsis(
Fingerprint("22222222222222222222222222222222222222222222"), Fingerprint("C"),
null, null,
RevocationState.notRevoked(), RevocationState.notRevoked(),
mapOf()) mapOf())
@ -29,21 +29,21 @@ class CertificationTest {
@Test @Test
fun `verify result of toString() on certification`() { fun `verify result of toString() on certification`() {
val certification = Certification(alice, "Bob <bob@example.org>", bob, Date()) val certification = Certification(alice, "Bob <bob@example.org>", bob, Date())
assertEquals("0000000000000000000000000000000000000000 (Alice <alice@pgpainless.org>) certifies [Bob <bob@example.org>] 1111111111111111111111111111111111111111", assertEquals("A certifies binding: Bob <bob@example.org> <-> B [120]",
certification.toString()) certification.toString())
} }
@Test @Test
fun `verify result of toString() on delegation`() { fun `verify result of toString() on delegation`() {
val delegation = Certification(alice, null, bob, Date()) val delegation = Certification(alice, null, bob, Date())
assertEquals("0000000000000000000000000000000000000000 (Alice <alice@pgpainless.org>) delegates to 1111111111111111111111111111111111111111", assertEquals("A certifies binding: null <-> B [120]",
delegation.toString()) delegation.toString())
} }
@Test @Test
fun `verify result of toString() on delegation with userId-less issuer`() { fun `verify result of toString() on delegation with userId-less issuer`() {
val delegation = Certification(charlie, null, bob, Date()) val delegation = Certification(charlie, null, bob, Date())
assertEquals("22222222222222222222222222222222222222222222 delegates to 1111111111111111111111111111111111111111", assertEquals("C certifies binding: null <-> B [120]",
delegation.toString()) delegation.toString())
} }
} }