From e7c6bda30b47ec11041d4e185ef51a891186d5c9 Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Mon, 31 Aug 2026 16:03:50 +0300 Subject: [PATCH] Show certificate expired message when signing with expired card --- .../DigiDoc/viewmodel/SigningViewModelTest.kt | 30 ---- .../shared/SharedContainerViewModelTest.kt | 92 ++++++++++ .../ee/ria/DigiDoc/exceptions/NFCError.kt | 8 + .../crypto/CryptoFileOpeningNavigation.kt | 3 + .../signing/FileOpeningNavigation.kt | 6 + .../DigiDoc/viewmodel/MobileIdViewModel.kt | 6 + .../ee/ria/DigiDoc/viewmodel/NFCViewModel.kt | 33 ++++ .../ria/DigiDoc/viewmodel/SigningViewModel.kt | 10 -- .../ria/DigiDoc/viewmodel/SmartIdViewModel.kt | 6 + app/src/main/res/values-et/strings.xml | 4 +- app/src/main/res/values/strings.xml | 4 +- libdigidoc-lib/build.gradle.kts | 1 + .../domain/model/ContainerWrapper.kt | 12 ++ .../domain/model/ContainerWrapperTest.kt | 91 ++++++++++ .../mobileId/MobileSignServiceImplTest.kt | 110 ++++++++++++ .../ria/DigiDoc/mobileId/MobileSignService.kt | 20 +++ .../MobileCreateSignatureProcessStatus.kt | 2 + .../SessionStatusResponseProcessStatus.kt | 2 + .../smartId/SmartSignServiceImplTest.kt | 159 ++++++++++++++++++ .../ria/DigiDoc/smartId/SmartSignService.kt | 20 +++ 20 files changed, 575 insertions(+), 44 deletions(-) create mode 100644 libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt index d92d8f117..a2ffbe0e1 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModelTest.kt @@ -245,36 +245,6 @@ class SigningViewModelTest { assertFalse(isEncryptButtonShown) } - @Test - fun signingViewModel_isShareButtonShown_returnTrue() = - runTest { - val file = - getResourceFileAsFile( - context, - "example_no_signatures.asice", - R.raw.example_no_signatures, - ) - - val container = SignedContainer.openOrCreate(context, file, listOf(file), true) - - val isShareButtonShown = viewModel.isShareButtonShown(container, false) - - assertTrue(isShareButtonShown) - } - - @Test - fun signingViewModel_isShareButtonShown_returnFalse() = - runTest { - val file = File.createTempFile("temp", ".txt") - file.writeText("test content") - - val container = SignedContainer.openOrCreate(context, file, listOf(file), true) - - val isShareButtonShown = viewModel.isShareButtonShown(container, false) - - assertFalse(isShareButtonShown) - } - @Test fun signingViewModel_isExistingContainer_returnTrue() = runTest { diff --git a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt index 16b689ebd..82d81a0b6 100644 --- a/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt +++ b/app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/shared/SharedContainerViewModelTest.kt @@ -41,6 +41,7 @@ import ee.ria.DigiDoc.configuration.repository.CentralConfigurationRepositoryImp import ee.ria.DigiDoc.configuration.repository.ConfigurationRepository import ee.ria.DigiDoc.configuration.repository.ConfigurationRepositoryImpl import ee.ria.DigiDoc.configuration.service.CentralConfigurationServiceImpl +import ee.ria.DigiDoc.cryptolib.CryptoContainer import ee.ria.DigiDoc.domain.model.ContainerFileOpeningResult import ee.ria.DigiDoc.domain.repository.fileopening.FileOpeningRepository import ee.ria.DigiDoc.libdigidoclib.SignedContainer @@ -585,6 +586,97 @@ class SharedContainerViewModelTest { assertFalse(viewModel.isNestedContainer(signedContainer)) } + @Test + fun sharedContainerViewModel_isNestedContainer_returnFalseForFreshContainerWhenStackClearedFirst() { + val staleContainer = SignedContainer(context) + val freshContainer = SignedContainer(context) + + viewModel.setSignedContainer(staleContainer) + + viewModel.clearContainers() + viewModel.setSignedContainer(freshContainer) + + assertEquals(1, viewModel.nestedContainers.size) + assertFalse(viewModel.isNestedContainer(freshContainer)) + } + + @Test + fun sharedContainerViewModel_isNestedContainer_returnTrueForFreshContainerWhenStaleContainerLeftInStack() { + val staleContainer = SignedContainer(context) + val freshContainer = SignedContainer(context) + + viewModel.setSignedContainer(staleContainer) + viewModel.setSignedContainer(freshContainer) + + assertEquals(2, viewModel.nestedContainers.size) + assertTrue(viewModel.isNestedContainer(freshContainer)) + } + + @Test + fun sharedContainerViewModel_setSignedContainer_doesNotGrowStackWhenSameContainerIsSetAgain() { + val signedContainer = SignedContainer(context) + + viewModel.setSignedContainer(signedContainer) + viewModel.setSignedContainer(signedContainer) + + assertEquals(1, viewModel.nestedContainers.size) + assertTrue(viewModel.nestedContainers.contains(signedContainer)) + assertFalse(viewModel.isNestedContainer(signedContainer)) + } + + @Test + fun sharedContainerViewModel_resetSignedContainer_leavesNestedContainerStackPopulated() { + val signedContainer = SignedContainer(context) + + viewModel.setSignedContainer(signedContainer) + viewModel.resetSignedContainer() + + assertNull(viewModel.signedContainer.value) + assertEquals(1, viewModel.nestedContainers.size) + } + + @Test + fun sharedContainerViewModel_clearContainers_makesFreshCryptoContainerNonNested() { + val staleContainer = SignedContainer(context) + val freshCryptoContainer = + CryptoContainer( + context = context, + file = File.createTempFile("crypto", ".cdoc"), + dataFiles = arrayListOf(), + recipients = arrayListOf(), + decrypted = false, + encrypted = false, + ) + + viewModel.setSignedContainer(staleContainer) + + viewModel.clearContainers() + viewModel.setCryptoContainer(freshCryptoContainer) + + assertEquals(1, viewModel.nestedContainers.size) + assertFalse(viewModel.isNestedContainer(freshCryptoContainer)) + } + + @Test + fun sharedContainerViewModel_isNestedContainer_returnTrueForFreshCryptoContainerWhenStaleContainerLeftInStack() { + val staleContainer = SignedContainer(context) + val freshCryptoContainer = + CryptoContainer( + context = context, + file = File.createTempFile("crypto", ".cdoc"), + dataFiles = arrayListOf(), + recipients = arrayListOf(), + decrypted = false, + encrypted = false, + ) + + viewModel.setSignedContainer(staleContainer) + viewModel.setCryptoContainer(freshCryptoContainer) + + assertEquals(2, viewModel.nestedContainers.size) + assertTrue(viewModel.isNestedContainer(freshCryptoContainer)) + } + @Test fun sharedContainerViewModel_setExternalFileUri_success() = runTest { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt index 25880619f..8646c8f52 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/exceptions/NFCError.kt @@ -67,6 +67,14 @@ sealed class NFCError { override val message: Int, ) : NFCError() + data class CertificateExpired( + override val message: Int, + ) : NFCError() + + data class CertificateNotYetValid( + override val message: Int, + ) : NFCError() + data class CertificateUnknown( override val message: Int, ) : NFCError() diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt index ff2b5c8b3..1468360c6 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/crypto/CryptoFileOpeningNavigation.kt @@ -145,6 +145,9 @@ fun CryptoFileOpeningNavigation( LaunchedEffect(cryptoFileOpeningViewModel.cryptoContainer) { cryptoFileOpeningViewModel.cryptoContainer.asFlow().collect { cryptoContainer -> cryptoContainer?.let { + if (!sharedContainerViewModel.nestedContainers.contains(it)) { + sharedContainerViewModel.clearContainers() + } sharedContainerViewModel.setCryptoContainer(it) delay(1000) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt index 009647035..0facd67c4 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/FileOpeningNavigation.kt @@ -219,6 +219,9 @@ fun FileOpeningNavigation( LaunchedEffect(fileOpeningViewModel.signedContainer) { fileOpeningViewModel.signedContainer.asFlow().collect { signedContainer -> signedContainer?.let { + if (!sharedContainerViewModel.nestedContainers.contains(it)) { + sharedContainerViewModel.clearContainers() + } sharedContainerViewModel.setSignedContainer(it) delay(1000) @@ -235,6 +238,9 @@ fun FileOpeningNavigation( LaunchedEffect(fileOpeningViewModel.cryptoContainer) { fileOpeningViewModel.cryptoContainer.asFlow().collect { cryptoContainer -> cryptoContainer?.let { + if (!sharedContainerViewModel.nestedContainers.contains(it)) { + sharedContainerViewModel.clearContainers() + } sharedContainerViewModel.setCryptoContainer(it) navController.navigate(Route.Encrypt.route) { diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt index 15579dfbc..bc4676290 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/MobileIdViewModel.kt @@ -131,6 +131,12 @@ class MobileIdViewModel ).put( MobileCreateSignatureProcessStatus.CERTIFICATE_REVOKED, R.string.signature_update_signature_error_message_certificate_revoked, + ).put( + MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED, + R.string.signature_update_signature_error_message_certificate_expired, + ).put( + MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID, + R.string.signature_update_signature_error_message_certificate_not_yet_valid, ).put( MobileCreateSignatureProcessStatus.GENERAL_ERROR, R.string.error_general_client, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt index d867c92c8..12eabc1d3 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/NFCViewModel.kt @@ -62,6 +62,7 @@ import ee.ria.DigiDoc.utils.pin.PinCodeUtil.isPINLengthValid import ee.ria.DigiDoc.utilsLib.extensions.clearSensitive import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil import ee.ria.libdigidocpp.ExternalSigner import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.flow.MutableStateFlow @@ -70,6 +71,8 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.bouncycastle.util.encoders.Hex +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import java.util.Base64 import javax.inject.Inject @@ -329,6 +332,12 @@ class NFCViewModel val message = ex.message.orEmpty() when { + ex is CertificateExpiredException -> + showExpiredCertificateError(ex) + + ex is CertificateNotYetValidException -> + showNotYetValidCertificateError(ex) + message.contains("Certificate status: revoked") -> showRevokedCertificateError(ex) @@ -695,6 +704,8 @@ class NFCViewModel } } + CertificateUtil.x509Certificate(signerCert).checkValidity() + val hashBytes = Base64.getDecoder().decode(hash) val (_, signatureArray) = idCardService.sign(card, pin2Code, hashBytes) @@ -712,6 +723,12 @@ class NFCViewModel val message = ex.message.orEmpty() when { + ex is CertificateExpiredException -> + showExpiredCertificateError(ex) + + ex is CertificateNotYetValidException -> + showNotYetValidCertificateError(ex) + message.contains("Certificate status: revoked") -> showRevokedCertificateError(ex) @@ -791,6 +808,22 @@ class NFCViewModel errorLog(logTag, "Unable to sign with NFC - Certificate status: revoked", e) } + private fun showExpiredCertificateError(e: Exception) { + _errorState.update { + NFCError.CertificateExpired(R.string.signature_update_signature_error_message_certificate_expired) + } + errorLog(logTag, "Unable to sign with NFC - Signing certificate has expired", e) + } + + private fun showNotYetValidCertificateError(e: Exception) { + _errorState.update { + NFCError.CertificateNotYetValid( + R.string.signature_update_signature_error_message_certificate_not_yet_valid, + ) + } + errorLog(logTag, "Unable to sign with NFC - Signing certificate is not yet valid", e) + } + private fun showUnknownCertificateError(e: Exception) { _errorState.update { NFCError.CertificateUnknown(R.string.signature_update_signature_error_message_certificate_unknown) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt index c13902378..819efd9aa 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SigningViewModel.kt @@ -142,16 +142,6 @@ class SigningViewModel ) && !isNestedContainer - fun isShareButtonShown( - signedContainer: SignedContainer?, - isNestedContainer: Boolean, - ): Boolean = - ( - isExistingContainer(signedContainer) || - !isContainerWithoutSignatures(signedContainer) - ) && - !isNestedContainer - fun isBottomContainerButtonShown( signedContainer: SignedContainer?, isNestedContainer: Boolean, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt index 65ced52f5..23c86b62b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModel.kt @@ -151,6 +151,12 @@ class SmartIdViewModel ).put( SessionStatusResponseProcessStatus.CERTIFICATE_REVOKED, R.string.signature_update_signature_error_message_certificate_revoked, + ).put( + SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED, + R.string.signature_update_signature_error_message_certificate_expired, + ).put( + SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID, + R.string.signature_update_signature_error_message_certificate_not_yet_valid, ).put( SessionStatusResponseProcessStatus.NOT_QUALIFIED, R.string.signature_update_smart_id_error_message_not_qualified, diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 35eff8865..e7b6e0d8a 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -320,6 +320,8 @@ https://www.id.ee/artikkel/id-kaardi-pin-ja-puk-koodide-muutmine/ Sertifikaat on kehtetu Sertifikaadi staatus on teadmata + Sertifikaat on aegunud + Sertifikaat pole veel kehtiv ID-kaardi Mobiil-ID Smart-ID @@ -344,10 +346,8 @@ Mobiilse allkirjastamise sessioon on lukustunud, proovi hiljem uuesti Viga mobiil-ID kasutamisel kasutaja telefonis Kasutaja ei ole mobiil-ID klient - Sinu mobiil-ID allkirjastamise sertifikaat on tühistatud Sinu mobiil-ID allkirjastamise sertifikaadi olek on teadmata Sinu mobiil-ID allkirjastamise sertifikaadi kehtivus on peatatud - Sinu mobiil-ID allkirjastamise sertifikaat on aegunud Liiga palju faile ühes konteineris Ümbriku allkirjastamine ebaõnnestus. Kontrolli mobiil-ID teenuse ligipääsu seadeid. https://www.id.ee/artikkel/asutustele-kus-allkirjastatakse-digidoc4-kliendi-kaudu-suuremates-kogustes-dokumente/ Samaaegsete pöördumiste limiit ületatud diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d23132282..dcc8a6722 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -320,6 +320,8 @@ https://www.id.ee/en/article/changing-id-card-pin-codes-and-puk-code/ Certificate status revoked Certificate status unknown + Certificate has expired + Certificate not yet valid ID-card\'s Mobile-ID Smart-ID @@ -344,10 +346,8 @@ Mobile signing session is locked, try again later An error occurred with the signers mobile phone User is not a mobile-ID client - Your mobile signing certificate is revoked Your mobile signing certificate status is unknown Your mobile signing certificate is suspended - Your mobile signing certificate is expired Too many datafiles in container Failed to sign container. Check your mobile-ID service access settings. https://www.id.ee/en/article/for-organisations-that-sign-large-quantities-of-documents-using-digidoc4-client/ Simultaneous requests limit exceeded diff --git a/libdigidoc-lib/build.gradle.kts b/libdigidoc-lib/build.gradle.kts index 11e208b84..cd4a0d8f5 100644 --- a/libdigidoc-lib/build.gradle.kts +++ b/libdigidoc-lib/build.gradle.kts @@ -71,6 +71,7 @@ dependencies { implementation(libs.androidx.hilt) testImplementation(libs.junit) + testImplementation(libs.mockito.kotlin) androidTestImplementation(libs.byte.buddy) androidTestImplementation(libs.mockito.android) { diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt index f2560f3d6..a3e25a29a 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapper.kt @@ -25,6 +25,7 @@ import ee.ria.DigiDoc.libdigidoclib.SignedContainer import ee.ria.DigiDoc.libdigidoclib.init.libdigidocppDispatcher import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog +import ee.ria.DigiDoc.utilsLib.signing.CertificateUtil import ee.ria.DigiDoc.utilsLib.text.TextUtil.removeEmptyStrings import ee.ria.libdigidocpp.ExternalSigner import ee.ria.libdigidocpp.Signature @@ -61,6 +62,7 @@ class ContainerWrapperImpl : ContainerWrapper { ): ByteArray = withContext(libdigidocppDispatcher) { debugLog(logTag, "Preparing signature (with role data: ${roleData != null})") + checkCertificateValidity(cert) signature = when { roleData != null && signedContainer != null -> { @@ -103,4 +105,14 @@ class ContainerWrapperImpl : ContainerWrapper { signedContainer?.rawContainer()?.save() debugLog(logTag, "Signature finalized and container saved") } + + @Throws(CertificateException::class) + private fun checkCertificateValidity(cert: ByteArray?) { + if (cert == null) { + throw CertificateException("Unable to validate a missing signing certificate") + } + val x509Certificate = CertificateUtil.x509Certificate(cert) + debugLog(logTag, "Signing certificate valid from ${x509Certificate.notBefore} to ${x509Certificate.notAfter}") + x509Certificate.checkValidity() + } } diff --git a/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt b/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt new file mode 100644 index 000000000..63813fd06 --- /dev/null +++ b/libdigidoc-lib/src/test/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/ContainerWrapperTest.kt @@ -0,0 +1,91 @@ +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.libdigidoclib.domain.model + +import ee.ria.libdigidocpp.ExternalSigner +import kotlinx.coroutines.runBlocking +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.mock +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException +import java.util.Calendar +import java.util.Date + +class ContainerWrapperTest { + private val containerWrapper = ContainerWrapperImpl() + private val signer = mock() + + @Test + fun containerWrapper_prepareSignature_throwsWhenSigningCertificateHasExpired() { + val expiredCertificate = certificate(fromYearsFromNow = -2, toYearsFromNow = -1) + + assertThrows(CertificateExpiredException::class.java) { + runBlocking { containerWrapper.prepareSignature(signer, null, expiredCertificate, null) } + } + } + + @Test + fun containerWrapper_prepareSignature_throwsWhenSigningCertificateIsNotYetValid() { + val futureCertificate = certificate(fromYearsFromNow = 1, toYearsFromNow = 2) + + assertThrows(CertificateNotYetValidException::class.java) { + runBlocking { containerWrapper.prepareSignature(signer, null, futureCertificate, null) } + } + } + + @Test + fun containerWrapper_prepareSignature_throwsCertificateExceptionWhenCertificateMissing() { + val exception = + assertThrows(CertificateException::class.java) { + runBlocking { containerWrapper.prepareSignature(signer, null, null, null) } + } + + assertTrue(exception.message?.contains("missing signing certificate") == true) + } + + @Test + fun containerWrapper_prepareSignature_passesCertificateCheckWhenCertificateIsCurrentlyValid() { + val validCertificate = certificate(fromYearsFromNow = -1, toYearsFromNow = 1) + + val exception = + assertThrows(IllegalStateException::class.java) { + runBlocking { containerWrapper.prepareSignature(signer, null, validCertificate, null) } + } + + assertTrue(exception.message?.contains("Unable to get container") == true) + } + + private fun certificate( + fromYearsFromNow: Int, + toYearsFromNow: Int, + ): ByteArray { + val keyPair = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair() + val name = X500Name("CN=RIA DigiDoc test") + val builder = + JcaX509v3CertificateBuilder( + name, + BigInteger.valueOf(1), + yearsFromNow(fromYearsFromNow), + yearsFromNow(toYearsFromNow), + name, + keyPair.public, + ) + val contentSigner = JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private) + return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)).encoded + } + + private fun yearsFromNow(years: Int): Date = + Calendar + .getInstance() + .apply { add(Calendar.YEAR, years) } + .time +} diff --git a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt index 35a522f0f..1cc1008af 100644 --- a/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt +++ b/mobile-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/mobileId/MobileSignServiceImplTest.kt @@ -70,6 +70,7 @@ import org.mockito.kotlin.doReturn import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.isNull +import org.mockito.kotlin.never import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import retrofit2.Call @@ -78,6 +79,8 @@ import retrofit2.mock.Calls import java.net.UnknownHostException import java.security.NoSuchAlgorithmException import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import javax.net.ssl.SSLPeerUnverifiedException @RunWith(MockitoJUnitRunner::class) @@ -376,6 +379,113 @@ class MobileSignServiceImplTest { request.relyingPartyUUID = RELYING_PARTY_UUID } + @Test + fun mobileSignService_processMobileIdRequest_prepareSignatureCertificateExpired() = + runTest { + doReturn(midRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sslContext = null, + midSignServiceUrl = request.url, + certBundle = certBundle, + trustManagers = trustManagers, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + doReturn( + Calls.response( + Response.success( + MobileCreateSignatureCertificateResponse( + result = MobileCertificateResultType.OK, + cert = cert, + ), + ), + ), + ).whenever(midRestServiceClient) + .getCertificate( + body = getMobileCertificateRequest, + ) + + whenever( + containerWrapper.prepareSignature( + signer = any(), + signedContainer = any(), + cert = any(), + roleData = isNull(), + ), + ).thenThrow(CertificateExpiredException()) + + mobileSignServiceImpl.processMobileIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + + verify(statusObserver, atLeastOnce()).onChanged(MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED) + + verify(containerWrapper, never()).finalizeSignature(any(), any(), any()) + } + + @Test + fun mobileSignService_processMobileIdRequest_prepareSignatureCertificateNotYetValid() = + runTest { + doReturn(midRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sslContext = null, + midSignServiceUrl = request.url, + certBundle = certBundle, + trustManagers = trustManagers, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + doReturn( + Calls.response( + Response.success( + MobileCreateSignatureCertificateResponse( + result = MobileCertificateResultType.OK, + cert = cert, + ), + ), + ), + ).whenever(midRestServiceClient) + .getCertificate( + body = getMobileCertificateRequest, + ) + + whenever( + containerWrapper.prepareSignature( + signer = any(), + signedContainer = any(), + cert = any(), + roleData = isNull(), + ), + ).thenThrow(CertificateNotYetValidException()) + + mobileSignServiceImpl.processMobileIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + + verify(statusObserver, atLeastOnce()) + .onChanged(MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID) + + verify(containerWrapper, never()).finalizeSignature(any(), any(), any()) + } + @Test fun mobileSignService_processMobileIdRequest_success() = runTest { diff --git a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt index 000748484..1affcf8ac 100644 --- a/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt +++ b/mobile-id-lib/src/main/kotlin/ee/ria/DigiDoc/mobileId/MobileSignService.kt @@ -75,6 +75,8 @@ import java.security.KeyStoreException import java.security.NoSuchAlgorithmException import java.security.UnrecoverableKeyException import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import java.util.Base64 import javax.inject.Inject import javax.inject.Singleton @@ -394,6 +396,24 @@ class MobileSignServiceImpl return } } + } catch (cee: CertificateExpiredException) { + errorLog( + logTag, + "Failed to sign with Mobile-ID. Signing certificate has expired. " + + "Exception message: ${cee.message}", + cee, + ) + postFault(RESTServiceFault(MobileCreateSignatureProcessStatus.CERTIFICATE_EXPIRED)) + return + } catch (cnyve: CertificateNotYetValidException) { + errorLog( + logTag, + "Failed to sign with Mobile-ID. Signing certificate is not yet valid. " + + "Exception message: ${cnyve.message}", + cnyve, + ) + postFault(RESTServiceFault(MobileCreateSignatureProcessStatus.CERTIFICATE_NOT_YET_VALID)) + return } catch (e: UnknownHostException) { errorLog( logTag, diff --git a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt index acbb7ab81..9f37c91af 100644 --- a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt +++ b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/mid/dto/response/MobileCreateSignatureProcessStatus.kt @@ -35,6 +35,8 @@ enum class MobileCreateSignatureProcessStatus { INVALID_ACCESS_RIGHTS, OCSP_INVALID_TIME_SLOT, CERTIFICATE_REVOKED, + CERTIFICATE_EXPIRED, + CERTIFICATE_NOT_YET_VALID, GENERAL_ERROR, NO_RESPONSE, INVALID_COUNTRY_CODE, diff --git a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt index c322818af..2bb18c166 100644 --- a/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt +++ b/networking-lib/src/main/kotlin/ee/ria/DigiDoc/network/sid/dto/response/SessionStatusResponseProcessStatus.kt @@ -44,6 +44,8 @@ enum class SessionStatusResponseProcessStatus { INVALID_ACCESS_RIGHTS, OCSP_INVALID_TIME_SLOT, CERTIFICATE_REVOKED, + CERTIFICATE_EXPIRED, + CERTIFICATE_NOT_YET_VALID, OLD_API, UNDER_MAINTENANCE, GENERAL_ERROR, diff --git a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt index ec3edaeb1..0320eb13b 100644 --- a/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt +++ b/smart-id-lib/src/androidTest/kotlin/ee/ria/DigiDoc/smartId/SmartSignServiceImplTest.kt @@ -79,6 +79,8 @@ import java.io.IOException import java.net.UnknownHostException import java.security.NoSuchAlgorithmException import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import javax.net.ssl.SSLPeerUnverifiedException @RunWith(MockitoJUnitRunner::class) @@ -387,6 +389,163 @@ class SmartSignServiceImplTest { verifyNoInteractions(call) } + @Test + fun smartSignService_processSmartIdRequest_prepareSignatureCertificateExpired() = + runTest { + doReturn(sidRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sidSignServiceUrl = request.url + "/", + certBundle = certBundle, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + doReturn( + Calls.response( + Response.success( + SessionResponse( + sessionID = "sessionID", + ), + ), + ), + ).whenever(sidRestServiceClient) + .getCertificateV2( + semanticsIdentifier = "PNOEE-60001019906", + body = getSmartCertificateRequest, + ) + + doReturn( + Calls.response( + Response.success( + SessionStatusResponse( + state = SessionStatusResponseProcessState.COMPLETE, + result = + SessionStatusResponseStatus( + endResult = SessionStatusResponseProcessStatus.OK, + documentNumber = "documentNumber", + ), + signature = + SmartSignatureResponse( + value = "dTzZ20E8kmoXXBAh5cV5yw==", + algorithm = "SmartIDSignatureAlgorithm", + ), + cert = + SmartCertificateResponse( + value = cert, + assuranceLevel = "assuranceLevel", + certificateLevel = "certificateLevel", + ), + ), + ), + ), + ).whenever(sidRestServiceClient) + .getSessionStatus("sessionID", 5000) + + whenever( + containerWrapper.prepareSignature( + signer = any(), + signedContainer = any(), + cert = any(), + roleData = isNull(), + ), + ).thenThrow(CertificateExpiredException()) + + smartSignServiceImpl.processSmartIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + + verify(statusObserver, atLeastOnce()).onChanged(SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED) + + verify(containerWrapper, never()).finalizeSignature(any(), any(), any()) + } + + @Test + fun smartSignService_processSmartIdRequest_prepareSignatureCertificateNotYetValid() = + runTest { + doReturn(sidRestServiceClient).whenever(serviceGenerator).createService( + context = context, + sidSignServiceUrl = request.url + "/", + certBundle = certBundle, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + ) + + doReturn( + Calls.response( + Response.success( + SessionResponse( + sessionID = "sessionID", + ), + ), + ), + ).whenever(sidRestServiceClient) + .getCertificateV2( + semanticsIdentifier = "PNOEE-60001019906", + body = getSmartCertificateRequest, + ) + + doReturn( + Calls.response( + Response.success( + SessionStatusResponse( + state = SessionStatusResponseProcessState.COMPLETE, + result = + SessionStatusResponseStatus( + endResult = SessionStatusResponseProcessStatus.OK, + documentNumber = "documentNumber", + ), + signature = + SmartSignatureResponse( + value = "dTzZ20E8kmoXXBAh5cV5yw==", + algorithm = "SmartIDSignatureAlgorithm", + ), + cert = + SmartCertificateResponse( + value = cert, + assuranceLevel = "assuranceLevel", + certificateLevel = "certificateLevel", + ), + ), + ), + ), + ).whenever(sidRestServiceClient) + .getSessionStatus("sessionID", 5000) + + whenever( + containerWrapper.prepareSignature( + signer = any(), + signedContainer = any(), + cert = any(), + roleData = isNull(), + ), + ).thenThrow(CertificateNotYetValidException()) + + smartSignServiceImpl.processSmartIdRequest( + context = context, + signedContainer = signedContainer, + request = request, + roleDataRequest = null, + proxySetting = proxySetting, + manualProxySettings = manualProxy, + certificateBundle = certBundle, + accessTokenPath = "accessTokenPath", + accessTokenPass = "accessTokenPass", + ) + + verify(statusObserver, atLeastOnce()) + .onChanged(SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID) + + verify(containerWrapper, never()).finalizeSignature(any(), any(), any()) + } + @Test fun smartSignService_processSmartIdRequest_success() = runTest { diff --git a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt index c3b1ac2ca..8ad9e2fbb 100644 --- a/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt +++ b/smart-id-lib/src/main/kotlin/ee/ria/DigiDoc/smartId/SmartSignService.kt @@ -64,6 +64,8 @@ import java.net.UnknownHostException import java.nio.charset.StandardCharsets import java.security.NoSuchAlgorithmException import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException import java.util.Base64 import javax.inject.Inject import javax.inject.Singleton @@ -323,6 +325,24 @@ class SmartSignServiceImpl setErrorState(errorString) return } + } catch (cee: CertificateExpiredException) { + errorLog( + logTag, + "Failed to sign with Smart-ID. Signing certificate has expired. " + + "Exception message: ${cee.message}", + cee, + ) + postFault(ServiceFault(SessionStatusResponseProcessStatus.CERTIFICATE_EXPIRED)) + return + } catch (cnyve: CertificateNotYetValidException) { + errorLog( + logTag, + "Failed to sign with Smart-ID. Signing certificate is not yet valid. " + + "Exception message: ${cnyve.message}", + cnyve, + ) + postFault(ServiceFault(SessionStatusResponseProcessStatus.CERTIFICATE_NOT_YET_VALID)) + return } catch (e: UnknownHostException) { postFault(ServiceFault(SessionStatusResponseProcessStatus.NO_RESPONSE)) errorLog(