From d931a7f93547f1bbed10c1eea94b302789c2412d Mon Sep 17 00:00:00 2001 From: Marten Rebane Date: Tue, 30 Jun 2026 16:08:36 +0300 Subject: [PATCH] Fix showing digital seals --- .../component/signing/SignatureComponent.kt | 17 +++- .../ui/component/signing/SigningNavigation.kt | 2 +- .../signing/certificate/SignerDetailItem.kt | 4 +- .../signing/certificate/SignerDetailsView.kt | 10 ++- .../domain/model/SignatureWrapperTest.kt | 89 +++++++++++++++++++ .../domain/model/SignatureInterface.kt | 1 + .../domain/model/SignatureWrapper.kt | 13 +++ 7 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapperTest.kt diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt index 45d0f0583..4b553213e 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SignatureComponent.kt @@ -111,7 +111,12 @@ fun SignatureComponent( } else { Column { signatures.forEachIndexed { index, signature -> - val nameText = formatName(signature.name) + val nameText = + if (signature.isDigitalSeal) { + signature.name + } else { + formatName(signature.name) + } val statusText = if (isTimestamped) { getTimestampStatusText(LocalContext.current, signature.validator.status) @@ -172,10 +177,15 @@ fun SignatureComponent( .wrapContentHeight(), contentAlignment = Alignment.Center, ) { - val iconTestTagSuffix = if (isTimestamped) "Timestamp" else "Signature" + val iconTestTagSuffix = + if (isTimestamped || signature.isDigitalSeal) { + "Timestamp" + } else { + "Signature" + } Icon( imageVector = - if (isTimestamped) { + if (isTimestamped || signature.isDigitalSeal) { ImageVector.vectorResource(R.drawable.ic_m3_approval_48dp_wght400) } else { ImageVector.vectorResource(R.drawable.ic_icon_signature) @@ -214,6 +224,7 @@ fun SignatureComponent( }.testTag("signatureComponentSignatureName"), name = nameText, allCaps = isTimestamped || showNameAsAllCaps, + formatName = !signature.isDigitalSeal, ) Text( text = signedTime, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt index c01dc659f..e44739b22 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SigningNavigation.kt @@ -1224,7 +1224,7 @@ fun SigningNavigation( modifier = modifier, showSheet = showSignatureBottomSheet, clickedSignature = clickedSignature, - isTimestamp = isTimestampedContainer, + isTimestamp = isTimestampedContainer || clickedSignature.value?.isDigitalSeal == true, signedContainer = signedContainer, signingViewModel = signingViewModel, sharedSignatureViewModel = sharedSignatureViewModel, diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt index 4c5a33018..59acdaa1b 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailItem.kt @@ -75,7 +75,9 @@ data class SignerDetailItem( SignerDetailItem( label = R.string.signers_certificate_label, value = - if (isTimestamp) { + if (signature.isDigitalSeal) { + signature.signedBy + } else if (isTimestamp) { NameUtil.formatName(signature.signedBy).uppercase() } else { NameUtil.formatName(signature.signedBy) diff --git a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt index 4656237a3..07de7a601 100644 --- a/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt +++ b/app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/certificate/SignerDetailsView.kt @@ -229,7 +229,12 @@ fun SignerDetailsView( ) Spacer(modifier = modifier.width(SPadding)) - val nameText = formatName(signature.name) + val nameText = + if (signature.isDigitalSeal) { + signature.name + } else { + formatName(signature.name) + } val statusText = getSignatureStatusText( LocalContext.current, @@ -258,7 +263,8 @@ fun SignerDetailsView( }.testTag("signatureDetailsSignatureName") .notAccessible(), nameText, - allCaps = isTimestamp, + allCaps = isTimestamp && signature.isDigitalSeal.not(), + formatName = signature.isDigitalSeal.not(), ) ColoredSignedStatusText( text = statusText, diff --git a/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapperTest.kt b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapperTest.kt new file mode 100644 index 000000000..5ad99637c --- /dev/null +++ b/libdigidoc-lib/src/androidTest/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapperTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +@file:Suppress("PackageName") + +package ee.ria.DigiDoc.libdigidoclib.domain.model + +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.CertificatePolicies +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.PolicyInformation +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.Date + +class SignatureWrapperTest { + @Test + fun signatureWrapper_isCertificateDigitalSeal_trueForESealOid_7_3() { + val certDer = generateCertWithPolicy("1.3.6.1.4.1.10015.7.3") + assertTrue(SignatureWrapper.isCertificateDigitalSeal(certDer)) + } + + @Test + fun signatureWrapper_isCertificateDigitalSeal_trueForESealOid_7_1() { + val certDer = generateCertWithPolicy("1.3.6.1.4.1.10015.7.1") + assertTrue(SignatureWrapper.isCertificateDigitalSeal(certDer)) + } + + @Test + fun signatureWrapper_isCertificateDigitalSeal_trueForESealOid_2_1() { + val certDer = generateCertWithPolicy("1.3.6.1.4.1.10015.2.1") + assertTrue(SignatureWrapper.isCertificateDigitalSeal(certDer)) + } + + @Test + fun signatureWrapper_isCertificateDigitalSeal_falseForIdCardOid() { + val certDer = generateCertWithPolicy("1.3.6.1.4.1.10015.1.1") + assertFalse(SignatureWrapper.isCertificateDigitalSeal(certDer)) + } + + @Test + fun signatureWrapper_isCertificateDigitalSeal_falseForEmptyByteArray() { + assertFalse(SignatureWrapper.isCertificateDigitalSeal(ByteArray(0))) + } + + private fun generateCertWithPolicy(policyOid: String): ByteArray { + val keyPairGen = KeyPairGenerator.getInstance("RSA") + keyPairGen.initialize(2048) + val keyPair = keyPairGen.generateKeyPair() + + val now = Date() + val notAfter = Date.from(Instant.now().plus(365, ChronoUnit.DAYS)) + val subject = X500Name("CN=Test") + + val builder = JcaX509v3CertificateBuilder(subject, BigInteger.ONE, now, notAfter, subject, keyPair.public) + builder.addExtension( + Extension.certificatePolicies, + false, + CertificatePolicies(arrayOf(PolicyInformation(ASN1ObjectIdentifier(policyOid)))), + ) + + val signer = JcaContentSignerBuilder("SHA256withRSA").build(keyPair.private) + return builder.build(signer).encoded + } +} diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt index d1d2c7f87..9cba8be9b 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureInterface.kt @@ -48,6 +48,7 @@ interface SignatureInterface : Serializable { val ocspCertificateDer: ByteArray val timeStampCertificateDer: ByteArray val archiveTimeStampCertificateDer: ByteArray + val isDigitalSeal: Boolean val validator: ValidatorInterface } diff --git a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt index 4a30593b7..fe86eea25 100644 --- a/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt +++ b/libdigidoc-lib/src/main/kotlin/ee/ria/DigiDoc/libdigidoclib/domain/model/SignatureWrapper.kt @@ -23,6 +23,7 @@ package ee.ria.DigiDoc.libdigidoclib.domain.model import ee.ria.DigiDoc.common.certificate.CertificateServiceImpl import ee.ria.DigiDoc.common.model.Certificate +import ee.ria.DigiDoc.common.model.EIDType import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog import ee.ria.libdigidocpp.Signature @@ -92,6 +93,18 @@ class SignatureWrapper( ByteArray(0) } + override val isDigitalSeal: Boolean = isCertificateDigitalSeal(signingCertificateDer) + + companion object { + fun isCertificateDigitalSeal(signingCertificateDer: ByteArray): Boolean = + try { + val certService = CertificateServiceImpl() + certService.extractEIDType(certService.parseCertificate(signingCertificateDer)) == EIDType.E_SEAL + } catch (_: Exception) { + false + } + } + override val validator: ValidatorInterface = ValidatorWrapper(Signature.Validator(signature)) private fun signatureName(signature: Signature): String {