Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break up Certificate#verify to top level Trifle APIs in android sdk #155

Merged
merged 4 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,21 @@ val certs: List<Certificate> = response.map { Certificate.deserialize(it) }

// Check if app has the root cert of Certificate Authority (CA).

// Validate cert matches the certificate request (so generated key)
// Validate cert matches the certificate request (so generated key)
val result: Result<Unit> = TrifleApi.verifyAttributes(certs[0], certReq)

// and the root (so it has been generated by the right CA).
val result: Result<Unit> = TrifleApi.verify(certs[0], certReq, certs, root)
val result: Result<Unit> = TrifleApi.verifyChain(certs, root)

// Once it passes validation, certReq is no longer needed, and it can be deleted
// Store cert along with the respective keyHandle

// To check only for the validity of a stored cert, you can do either of below choices
// Option 1 is a more complete check of the device cert and the full chain
val result: Result<Unit> = TrifleApi.verify(cert, ancestorCertificateChain = certs.drop(1))
val result: Result<Unit> = TrifleApi.verifyChain(certs)

// option 2 only checks the validity of the device cert
val result: Result<Unit> = TrifleApi.verify(cert)
val result: Result<Unit> = TrifleApi.verifyValidity(cert)
gadphly marked this conversation as resolved.
Show resolved Hide resolved

// Error handling from the Result.isFailure can be found in the enumeration in TrifleErrors
if (result.isFailure) {
Expand Down
1 change: 1 addition & 0 deletions android/trifle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation("junit:junit:4.13.2")
androidTestImplementation(project(":jvm-testing"))

api(project(":jvm"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package app.cash.trifle

import app.cash.trifle.TrifleErrors.CSRMismatch
import app.cash.trifle.TrifleErrors.ExpiredCertificate
import app.cash.trifle.TrifleErrors.NoTrustAnchor
import app.cash.trifle.TrifleErrors.NotValidYetCertificate
import app.cash.trifle.testing.TestCertificateAuthority
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException

import java.time.Duration
import java.time.Instant
import java.util.Date

class TrifleApiTest {
@JvmField
Expand Down Expand Up @@ -35,4 +42,85 @@ class TrifleApiTest {
@Test fun testDeleteKeyHandle() {
TrifleApi.delete(keyHandle)
}

@Test
fun testVerify_succeeds() {
val result = TrifleApi.verifyChain(
certificateChain = endEntity.certChain
)
assertTrue(result.isSuccess)
}

@Test
fun testVerifyCertificateValidity_succeeds() {
val result = TrifleApi.verifyValidity(
endEntity.certificate
)
assertTrue(result.isSuccess)
}

@Test
fun testVerifyAttributes_succeeds() {
val result = TrifleApi.verifyAttributes(
endEntity.certificate,
endEntity.certRequest
)
assertTrue(result.isSuccess)
}

@Test
fun testVerify_failsWithNoTrustAnchorForADifferentRootCertificate() {
val result = TrifleApi.verifyChain(
certificateChain = listOf(endEntity.certificate) + otherEndEntity.certChain.drop(1)
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NoTrustAnchor)
}

@Test
fun testVerify_failsWithExpiredCertificateForAnExpiredCertificate() {
val result = TrifleApi.verifyChain(
certificateChain = endEntity.certChain,
date = Date.from(Instant.now().plus(Duration.ofDays(365)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun testVerify_failsWithExpiredCertificateForAnExpiredStoredCertificate() {
val result = TrifleApi.verifyValidity(
endEntity.certificate,
Date.from(Instant.now().plus(Duration.ofDays(365)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is ExpiredCertificate)
}

@Test
fun testVerify_failsWithNotYetValidCertificateForAStoredCertificateYetToBeValid() {
val result = TrifleApi.verifyValidity(
endEntity.certificate,
date = Date.from(Instant.now().minus(Duration.ofDays(1)))
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is NotValidYetCertificate)
}

@Test
fun testVerifyAttributes_failsWithCSRMismatch() {
val result = TrifleApi.verifyAttributes(
endEntity.certificate,
otherEndEntity.certRequest
)
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is CSRMismatch)
}

companion object {
private val certificateAuthority = TestCertificateAuthority("issuingEntity")
private val otherCertificateAuthority = TestCertificateAuthority("otherIssuingEntity")
private val endEntity = certificateAuthority.createTestEndEntity("entity")
private val otherEndEntity = otherCertificateAuthority.createTestEndEntity("otherEntity")
}
}
64 changes: 63 additions & 1 deletion android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package app.cash.trifle

import app.cash.trifle.validators.CertChainValidatorFactory
import app.cash.trifle.validators.CertificateValidatorFactory
import java.util.Date

object TrifleApi {
/**
* Create a new mobile Trifle keypair for which can be used to create a
Expand Down Expand Up @@ -50,7 +54,8 @@ object TrifleApi {
*
* @param data - raw data to be signed.
* @param keyHandle - key handle used for the signing.
* @param certificates - certificate chain to be included in the SignedData message. Must match the key in keyHandle.
* @param certificates - certificate chain to be included in the SignedData message.
* Must match the key in keyHandle.
*
* @return A signed data message in the Trifle format [SignedData].
*/
Expand All @@ -59,4 +64,61 @@ object TrifleApi {
keyHandle: KeyHandle,
certificates: List<Certificate>
): SignedData = Trifle.EndEntity(keyHandle.keyPair).createSignedData(data, certificates)

/**
* Verify that the provided Trifle Certificate Chain is valid.
*
* @param certificateChain - list of certificates. Namely, the first
* entry should be the certificate corresponding to the subject, and the subsequent being
* the issuer of the former certificate, and each thereafter should be the issuer of the
* one before it.
* @param anchorCertificate - the trust anchor against which we would like to verify the
* certificateChain instead. This may be the terminal (root) certificate of the chain or may be
* an intermediate certificate in the chain which is already trusted.
* @param date - The date to use for verification against certificates' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyChain(
certificateChain: List<Certificate>,
anchorCertificate: Certificate? = null,
date: Date? = null
): Result<Unit> = CertChainValidatorFactory.get(
certAnchor = anchorCertificate ?: certificateChain.last(),
date = date
).validate(certificateChain)

/**
* Verify that the provided Trifle Certificate is valid.
*
* @param certificate - the certificate to verify
* @param date - The date to use for verification against certificate' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyValidity(
certificate: Certificate,
date: Date? = null
): Result<Unit> = CertificateValidatorFactory.get(certificate).validate(date)

/**
* Verify that the provided Trifle Certificate matches the Certificate Requests' attributes.
*
* @param certificate - the certificate to verify
* @param certificateRequest - request used to generate this certificate
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verifyAttributes(
certificate: Certificate,
certificateRequest: CertificateRequest
): Result<Unit> = CertificateValidatorFactory.get(certificate).validate(certificateRequest)
}
50 changes: 0 additions & 50 deletions jvm/src/main/kotlin/app/cash/trifle/Certificate.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
package app.cash.trifle

import app.cash.trifle.CertificateRequest.PKCS10Request
import app.cash.trifle.TrifleErrors.CSRMismatch
import app.cash.trifle.internal.validators.CertChainValidatorFactory
import okio.ByteString.Companion.toByteString
import org.bouncycastle.cert.X509CertificateHolder
import java.util.Date
import app.cash.trifle.protos.api.alpha.Certificate as CertificateProto

/**
Expand All @@ -27,51 +22,6 @@ data class Certificate internal constructor(
version = CERTIFICATE_VERSION
).encode()

/**
* Verify that the provided certificate matches what we expected.
* It matches the CSR that we have and the root cert is what
* we expect.
*
* @param certificateRequest - request used to generate this certificate
* @param ancestorCertificateChain - list of certificates preceding *this* one. Namely, the first
* entry should be the certificate corresponding to the issuer of this certificate, and each
* thereafter should be the issuer of the one before it.
* @param anchorCertificate - the trust anchor against which we would like to verify the
* ancestorCertificateChain. This may be the terminal (root) certificate of the chain or may be
* an intermediate certificate in the chain which is already trusted.
* @param date - The date to use for verification against certificates' validity windows. If null,
* the current time is used.
*
* @return - [Result] indicating [Result.isSuccess] or [Result.isFailure]:
* - success value is expressed as a [Unit] (Nothing)
* - failure value is expressed as a [TrifleErrors]
*/
fun verify(
certificateRequest: CertificateRequest,
ancestorCertificateChain: List<Certificate>,
anchorCertificate: Certificate,
date: Date? = null
): Result<Unit> {
// First check to see if the certificate chain validates
val certChainResult = CertChainValidatorFactory.get(anchorCertificate, date)
.validate(listOf(this) + ancestorCertificateChain)

return certChainResult.mapCatching {
val x509Certificate = X509CertificateHolder(certificate)
when (certificateRequest) {
is PKCS10Request -> {
// Certificate chain matches, check with certificate request.
// TODO(dcashman): Check other attributes as well.
if (certificateRequest.pkcs10Req.subject != x509Certificate.subject ||
certificateRequest.pkcs10Req.subjectPublicKeyInfo != x509Certificate.subjectPublicKeyInfo
) {
throw CSRMismatch
}
}
}
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
Expand Down
2 changes: 1 addition & 1 deletion jvm/src/main/kotlin/app/cash/trifle/SignedData.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package app.cash.trifle
import app.cash.trifle.TrifleErrors.InvalidSignature
import app.cash.trifle.internal.TrifleAlgorithmIdentifier
import app.cash.trifle.internal.providers.JCAContentVerifierProvider
import app.cash.trifle.internal.validators.CertChainValidatorFactory
import app.cash.trifle.validators.CertChainValidatorFactory
import okio.ByteString.Companion.toByteString
import app.cash.trifle.protos.api.alpha.Certificate as CertificateProto
import app.cash.trifle.protos.api.alpha.SignedData as SignedDataProto
Expand Down
1 change: 1 addition & 0 deletions jvm/src/main/kotlin/app/cash/trifle/TrifleErrors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ sealed class TrifleErrors(message: String, cause: Throwable? = null) : Exception
object NoTrustAnchor : TrifleErrors("No acceptable Trifle trust anchor found")
object InvalidCertPath : TrifleErrors("Invalid Trifle certificate path found")
object ExpiredCertificate : TrifleErrors("Expired Trifle certificate")
object NotValidYetCertificate : TrifleErrors("Trifle certificate is not valid yet")
object InvalidSignature : TrifleErrors("Invalid Trifle signature")
object CSRMismatch : TrifleErrors("Trifle certificate does not match CSR")
class UnspecifiedFailure(message: String, cause: Throwable) : TrifleErrors(message, cause)
Expand Down

This file was deleted.

Loading