diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/Container.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/Container.h index 92655e97..cdeac1c6 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/Container.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/Container.h @@ -75,7 +75,10 @@ class DIGIDOCPP_EXPORT Container template static void addContainerImplementation(); + static std::unique_ptr extendContainerValidity(Container &doc, Signer *signer, size_t &extendedCount); + protected: + virtual std::string path() const; Container(); unsigned int newSignatureId() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h index f6b81962..d0776490 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Headers/crypto/X509Cert.h @@ -96,6 +96,7 @@ namespace digidoc std::vector qcStatements() const; bool isCA() const; bool isValid(time_t *t = nullptr) const; + bool verify(bool noqscd, tm validation_time = {}) const; X509* handle() const; operator std::vector() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist index cb99f7de..b2d99a6b 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/Info.plist @@ -17,14 +17,14 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.3.0 + 4.5.0 CFBundleSignature ???? CFBundleVersion - 46 + 51 CSResourcesFileMapped MinimumOSVersion - 15.0 + 16.0 diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp index 26358d78..49d68f27 100755 Binary files a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp and b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/digidocpp differ diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd new file mode 100644 index 00000000..5df405d7 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd @@ -0,0 +1,43 @@ + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs + +X509CertificateLocation element was specified in TS 102 231 v3.1.2 clause B.4.3 The ServiceDigitalIdentityType. It is now deprecated and is not used + +PublicKeyLocation element was specified in TS 102 231 v3.1.2 clause B.4.3 The ServiceDigitalIdentityType. It is now deprecated and is not used + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.3 OtherCriteria, bullet 1) ExtendedKeyUsage + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.3 TakenOverBy Extension + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.3 OtherCriteria, bullet 2) CertSubjectDNAttribute + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd new file mode 100644 index 00000000..440b006c --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd @@ -0,0 +1,92 @@ + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2 Qualifications Extension + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.1 QualificationElement + + + + + + + + + + Please first try to use the CriteriaList before doing the OtherCriteria extension point. + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2 CriteriaList + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.3 Qualifier + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.2 PolicySet + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.1 KeyUsage + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd new file mode 100644 index 00000000..8c9513b8 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd @@ -0,0 +1,459 @@ + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5 Scheme operator address + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5.1 Scheme operator postal address + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5.2 Scheme operator electronic address + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.17 Scheme extensions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.18 Trust Service Provider List + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3 Scheme information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.3 TSL type + + Specified in TS 119 612 v2.1.1 clause 5.3.4 Scheme operator name + + Specified in TS 119 612 v2.1.1 clause 5.3.6 Scheme name + + Specified in TS 119 612 v2.1.1 clause 5.3.7 Scheme information URI + + + + Specified in TS 119 612 v2.1.1 clause 5.3.9 Scheme type/community/rules + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.10 Scheme territory + + + Specified in TS 119 612 v2.1.1 clause 5.3.11 TSL policy/legal notice + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.15 Next update + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs + + + + + + + + + + + + + + + + + + pecified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs item b) from Format + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs item c) from Format + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.16 Distribution points + + + Specified in TS 119 612 v2.1.1 clause 5.3.18 Trust Service Provider List + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.4 TSP information + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.4.6 TSP Services (list of services) + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5 Service information + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.4 Service current status + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.7 Service supply points + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.1 Service type identifier + + + Specified in TS 119 612 v2.1.1 clause 5.5.3 Service digital identity + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.10 Service history + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.6 Service history instance + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.4 expiredCertsRevocationInfo Extension + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.1 additionalServiceInformation Extension + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/xml.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/xml.xsd new file mode 100644 index 00000000..3f4e8541 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64/digidocpp.framework/schema/xml.xsd @@ -0,0 +1,117 @@ + + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/Container.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/Container.h index 92655e97..cdeac1c6 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/Container.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/Container.h @@ -75,7 +75,10 @@ class DIGIDOCPP_EXPORT Container template static void addContainerImplementation(); + static std::unique_ptr extendContainerValidity(Container &doc, Signer *signer, size_t &extendedCount); + protected: + virtual std::string path() const; Container(); unsigned int newSignatureId() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h index f6b81962..d0776490 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Headers/crypto/X509Cert.h @@ -96,6 +96,7 @@ namespace digidoc std::vector qcStatements() const; bool isCA() const; bool isValid(time_t *t = nullptr) const; + bool verify(bool noqscd, tm validation_time = {}) const; X509* handle() const; operator std::vector() const; diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist index cb99f7de..b2d99a6b 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/Info.plist @@ -17,14 +17,14 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 4.3.0 + 4.5.0 CFBundleSignature ???? CFBundleVersion - 46 + 51 CSResourcesFileMapped MinimumOSVersion - 15.0 + 16.0 diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp index 7089aa70..150cf1d3 100755 Binary files a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp and b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/digidocpp differ diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd new file mode 100644 index 00000000..5df405d7 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_additionaltypes_xsd.xsd @@ -0,0 +1,43 @@ + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs + +X509CertificateLocation element was specified in TS 102 231 v3.1.2 clause B.4.3 The ServiceDigitalIdentityType. It is now deprecated and is not used + +PublicKeyLocation element was specified in TS 102 231 v3.1.2 clause B.4.3 The ServiceDigitalIdentityType. It is now deprecated and is not used + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.3 OtherCriteria, bullet 1) ExtendedKeyUsage + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.3 TakenOverBy Extension + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.3 OtherCriteria, bullet 2) CertSubjectDNAttribute + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd new file mode 100644 index 00000000..440b006c --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020101_sie_xsd.xsd @@ -0,0 +1,92 @@ + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2 Qualifications Extension + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.1 QualificationElement + + + + + + + + + + Please first try to use the CriteriaList before doing the OtherCriteria extension point. + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2 CriteriaList + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.3 Qualifier + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.2 PolicySet + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.2.2.1 KeyUsage + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd new file mode 100644 index 00000000..8c9513b8 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/ts_119612v020201_201601xsd.xsd @@ -0,0 +1,459 @@ + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5 Scheme operator address + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5.1 Scheme operator postal address + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.5.2 Scheme operator electronic address + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.17 Scheme extensions + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.1.4 Language support + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.18 Trust Service Provider List + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3 Scheme information + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.3 TSL type + + Specified in TS 119 612 v2.1.1 clause 5.3.4 Scheme operator name + + Specified in TS 119 612 v2.1.1 clause 5.3.6 Scheme name + + Specified in TS 119 612 v2.1.1 clause 5.3.7 Scheme information URI + + + + Specified in TS 119 612 v2.1.1 clause 5.3.9 Scheme type/community/rules + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.10 Scheme territory + + + Specified in TS 119 612 v2.1.1 clause 5.3.11 TSL policy/legal notice + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.15 Next update + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs + + + + + + + + + + + + + + + + + + pecified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs item b) from Format + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.13 Pointers to other TSLs item c) from Format + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.3.16 Distribution points + + + Specified in TS 119 612 v2.1.1 clause 5.3.18 Trust Service Provider List + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.4 TSP information + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.4.6 TSP Services (list of services) + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5 Service information + + + + + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.4 Service current status + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.7 Service supply points + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.1 Service type identifier + + + Specified in TS 119 612 v2.1.1 clause 5.5.3 Service digital identity + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.10 Service history + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.6 Service history instance + + + + + + + + + + + + + + + + + + + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.4 expiredCertsRevocationInfo Extension + + + Specified in TS 119 612 v2.1.1 clause 5.5.9.1 additionalServiceInformation Extension + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/xml.xsd b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/xml.xsd new file mode 100644 index 00000000..3f4e8541 --- /dev/null +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/Libs/digidocpp.xcframework/ios-arm64_x86_64-simulator/digidocpp.framework/schema/xml.xsd @@ -0,0 +1,117 @@ + + + + + + + See http://www.w3.org/XML/1998/namespace.html and + http://www.w3.org/TR/REC-xml for information about this namespace. + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. + + Note that local names in this namespace are intended to be defined + only by the World Wide Web Consortium or its subgroups. The + following names are currently defined in this namespace and should + not be used with conflicting semantics by any Working Group, + specification, or document instance: + + base (as an attribute name): denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification. + + lang (as an attribute name): denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification. + + space (as an attribute name): denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification. + + Father (in any context at all): denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: + + In appreciation for his vision, leadership and dedication + the W3C XML Plenary on this 10th day of February, 2000 + reserves for Jon Bosak in perpetuity the XML name + xml:Father + + + + + This schema defines attributes and an attribute group + suitable for use by + schemas wishing to allow xml:base, xml:lang or xml:space attributes + on elements they define. + + To enable this, such a schema must import this schema + for the XML namespace, e.g. as follows: + <schema . . .> + . . . + <import namespace="http://www.w3.org/XML/1998/namespace" + schemaLocation="http://www.w3.org/2001/03/xml.xsd"/> + + Subsequently, qualified reference to any of the attributes + or the group defined below will have the desired effect, e.g. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + http://www.w3.org/2001/03/xml.xsd. + At the date of issue it can also be found at + http://www.w3.org/2001/xml.xsd. + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML Schema + itself. In other words, if the XML Schema namespace changes, the version + of this document at + http://www.w3.org/2001/xml.xsd will change + accordingly; the version at + http://www.w3.org/2001/03/xml.xsd will not change. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h index ff85448e..76318384 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.h @@ -39,6 +39,15 @@ NS_ASSUME_NONNULL_BEGIN + (NSString *)libdigidocppVersion; + (NSString *)mediaType; ++ (void)extendLastSignatureToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion; + +// Extends validity of all signatures. Legacy containers (DDOC, BDOC time-mark) are wrapped into a new +// ASiC-S container (written to outputAsicsPath) with an archive timestamp. ASiC-E containers are extended in place. +// The completion returns the path that was actually written. ++ (void)extendContainerToLTA:(NSString *)containerPath + outputAsicsPath:(NSString *)outputAsicsPath + completion:(void (^)(NSString * _Nullable savedPath, NSError * _Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm index b101549c..36baaa9a 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Container/DigiDocContainerWrapper.mm @@ -24,6 +24,7 @@ #import #import #import +#import #import "DigiDocContainerWrapper.h" #import "../Model/DigiDocContainer.h" @@ -31,6 +32,10 @@ #import "../Model/DigiDocSignature.h" #import "Exception/Util/ExceptionUtil.h" +@interface DigiDocContainerWrapper () ++ (DigiDocSignatureStatus)determineSignatureStatus:(int)status; +@end + struct DigiDocContainerOpenCB: public digidoc::ContainerOpenCB { private: bool validate; @@ -44,6 +49,50 @@ virtual bool validateOnline() const override { } }; +class TimestampSigner: public digidoc::Signer { +public: + explicit TimestampSigner(const std::string &userAgent) { + setUserAgent(userAgent); + } + + digidoc::X509Cert cert() const override { + return digidoc::X509Cert(); + } + + std::vector sign(const std::string &, const std::vector &) const override { + throw digidoc::Exception(__FILE__, __LINE__, "TimestampSigner cannot sign. It only creates timestamps."); + } +}; + +static bool isSignatureInvalid(const digidoc::Signature *signature) { + digidoc::Signature::Validator validator(signature); + return [DigiDocContainerWrapper determineSignatureStatus:validator.status()] == Invalid; +} + +static bool canExtend(const std::vector &signatures, const std::string &mediaType) { + if (signatures.empty()) { + return false; + } + // An ASiC-S timestamped container is re-timestamped in place. + // It must not be wrapped into another ASiC-S. + if (mediaType == "application/vnd.etsi.asic-s+zip") { + return true; + } + if (mediaType != "application/vnd.etsi.asic-e+zip") { + return false; + } + for (const auto *signature : signatures) { + if (signature->profile().find("time-stamp") == std::string::npos) { + return false; + } + // Invalid signatures cannot be extended in place — wrap the container into an ASiC-S container instead. + if (isSignatureInvalid(signature)) { + return false; + } + } + return true; +} + @implementation DigiDocContainerWrapper {} + (void)dispatch:(void (^)(void))command completion:(void (^)(NSError *error))completion { @@ -86,6 +135,7 @@ + (NSData *)getNSDataFromVector:(const std::vector&)vectorData { return [NSData dataWithBytes:vectorData.data() length:vectorData.size()]; } + + (DigiDocSignatureStatus)determineSignatureStatus:(int)status { typedef digidoc::Signature::Validator::Status Status; @@ -106,7 +156,7 @@ + (DigiDocSignature *)getSignature:(digidoc::Signature *)signature pos:(int)pos DigiDocSignature *digiDocSignature = [DigiDocSignature new]; digiDocSignature.signingCert = [DigiDocContainerWrapper getNSDataFromVector:signingCert]; - digiDocSignature.timestampCert = [DigiDocContainerWrapper getNSDataFromVector:ocspCert]; + digiDocSignature.timestampCert = [DigiDocContainerWrapper getNSDataFromVector:timestampCert]; digiDocSignature.ocspCert = [DigiDocContainerWrapper getNSDataFromVector:ocspCert]; std::string givename = signingCert.subjectName("GN"); @@ -133,6 +183,16 @@ + (DigiDocSignature *)getSignature:(digidoc::Signature *)signature pos:(int)pos digiDocSignature.messageImprint = [NSData dataWithBytes:signature->messageImprint().data() length:signature->messageImprint().size()]; digiDocSignature.trustedSigningTime = [NSString stringWithUTF8String:signature->trustedSigningTime().c_str()]; + auto archiveTimestamps = signature->ArchiveTimeStamps(); + if (!archiveTimestamps.empty()) { + const auto& firstTS = archiveTimestamps.front(); + digiDocSignature.archiveTimestampTime = [NSString stringWithUTF8String:firstTS.time.c_str()]; + digiDocSignature.archiveTimestampCert = [DigiDocContainerWrapper getNSDataFromVector:firstTS.cert]; + } else { + digiDocSignature.archiveTimestampTime = @""; + digiDocSignature.archiveTimestampCert = [NSData data]; + } + std::vector signerRoles = signature->signerRoles(); NSMutableArray* signerRolesList = [NSMutableArray arrayWithCapacity: signerRoles.size()]; for (auto const& signerRole: signerRoles) { @@ -358,5 +418,52 @@ + (void)removeDataFileFromContainerWithPath:(NSString *)containerPath atIndex:(N } completion:completion]; } ++ (void)extendLastSignatureToLTA:(NSString *)containerPath completion:(void (^)(NSError * _Nullable error))completion { + [self open:containerPath validateOnline:YES command:^(digidoc::Container &container) { + auto sigs = container.signatures(); + if (!sigs.empty()) { + TimestampSigner signer(digidoc::userAgent()); + signer.setProfile("time-stamp-archive"); + sigs.back()->extendSignatureProfile(&signer); + } + container.save(containerPath.UTF8String); + } completion:completion]; +} + ++ (void)extendContainerToLTA:(NSString *)containerPath + outputAsicsPath:(NSString *)outputAsicsPath + completion:(void (^)(NSString * _Nullable savedPath, NSError * _Nullable error))completion { + __block NSString *savedPath = nil; + [self open:containerPath validateOnline:YES command:^(digidoc::Container &container) { + auto signatures = container.signatures(); + const std::string mediaType = container.mediaType(); + + if (canExtend(signatures, mediaType)) { + TimestampSigner signer(digidoc::userAgent()); + signer.setProfile("time-stamp-archive"); + for (auto *signature : signatures) { + signature->extendSignatureProfile(&signer); + } + container.save(containerPath.UTF8String); + savedPath = containerPath; + } else { + auto asics = digidoc::Container::createPtr(outputAsicsPath.UTF8String); + if (!asics) { + throw digidoc::Exception(__FILE__, __LINE__, "Failed to create ASiC-S container"); + } + TimestampSigner signer(digidoc::userAgent()); + signer.setProfile("TimeStampToken"); + asics->addDataFile(containerPath.UTF8String, mediaType); + asics->sign(&signer); + asics->save(outputAsicsPath.UTF8String); + savedPath = outputAsicsPath; + } + } completion:^(NSError * _Nullable error) { + if (completion) { + completion(error ? nil : savedPath, error); + } + }]; +} + @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h index da66b2f6..bdb2b350 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h +++ b/Modules/LibdigidocLib/Sources/LibdigidocObjC/include/Model/DigiDocSignature.h @@ -52,5 +52,8 @@ typedef NS_ENUM(int, DigiDocSignatureStatus) { @property (nonatomic, assign) DigiDocSignatureStatus status; @property (nonatomic, strong) NSString *diagnosticsInfo; +@property (nonatomic, strong) NSString *archiveTimestampTime; +@property (nonatomic, strong) NSData *archiveTimestampCert; + @end diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift index ce3cc7c8..f9f05556 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapper.swift @@ -64,6 +64,10 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { return self.mediatype } + public func getContainerURL() async -> URL { + return self.containerURL + } + @MainActor public func saveDataFile(dataFile: DataFileWrapper, to directory: URL?) async throws -> URL { let savedFilesDirectory = try directory ?? Directories.getCacheDirectory( @@ -263,6 +267,71 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { } } + @discardableResult + public func extendSignatureToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol { + do { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DigiDocContainerWrapper.extendLastSignature(toLTA: containerFile.resolvedPath) { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + return try await open(containerFile: containerFile, isSivaConfirmed: true) + } catch { + let nsError = (error as NSError?) ?? + NSError(domain: "ContainerWrapper - cannot extend signature to LTA", code: 8) + throw DigiDocError.signatureExtensionFailed(ErrorDetail(nsError: nsError)) + } + } + + @discardableResult + public func extendSignaturesToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol { + do { + ContainerWrapper.logger().info("Extending signatures to LTA") + let outputURL = containerFile + .deletingPathExtension() + .appendingPathExtension(CommonsLib.Constants.Extension.Asics) + + let savedPath: String = try await withCheckedThrowingContinuation { continuation in + DigiDocContainerWrapper.extendContainer( + toLTA: containerFile.resolvedPath, + outputAsicsPath: outputURL.resolvedPath + ) { resultPath, error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: resultPath ?? containerFile.resolvedPath) + } + } + } + + let isWrappedIntoAsics = savedPath != containerFile.resolvedPath + ContainerWrapper.logger().info( + "Extended signatures to LTA. Wrapped into new ASiC-S: \(isWrappedIntoAsics, privacy: .public)" + ) + + if isWrappedIntoAsics { + do { + try fileManager.removeItem(at: containerFile) + } catch { + ContainerWrapper.logger().error( + "Unable to remove original container after wrapping to ASiC-S: \(error, privacy: .public)" + ) + } + } + + return try await open(containerFile: URL(fileURLWithPath: savedPath), isSivaConfirmed: true) + } catch { + ContainerWrapper.logger().error("Unable to extend signatures to LTA: \(error, privacy: .public)") + let nsError = (error as NSError?) ?? + NSError(domain: "ContainerWrapper - cannot extend signatures to LTA", code: 9) + throw DigiDocError.signatureExtensionFailed(ErrorDetail(nsError: nsError)) + } + } + private static func signatureStatusToDigiDocStatus(_ status: DigiDocSignatureStatus) -> SignatureStatus { switch status { case .Valid: @@ -339,7 +408,9 @@ public actor ContainerWrapper: ContainerWrapperProtocol, Loggable { status: signatureStatusToDigiDocStatus(signature.status), format: signature.format, messageImprint: signature.messageImprint, - diagnosticsInfo: signature.diagnosticsInfo + diagnosticsInfo: signature.diagnosticsInfo, + archiveTimestampTime: signature.archiveTimestampTime, + archiveTimestampCert: signature.archiveTimestampCert ) } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift index 316f52b6..36348c51 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Container/ContainerWrapperProtocol.swift @@ -26,6 +26,7 @@ public protocol ContainerWrapperProtocol: Sendable { func getSignatures() async -> [SignatureWrapper] func getDataFiles() async -> [DataFileWrapper] func getMimetype() async -> String + func getContainerURL() async -> URL func create(file: URL, dataFiles: [String]) async throws func open(containerFile: URL, isSivaConfirmed: Bool) async throws -> ContainerWrapper @discardableResult func addDataFiles(containerFile: URL, dataFiles: [URL]) async throws -> ContainerWrapperProtocol @@ -39,6 +40,8 @@ public protocol ContainerWrapperProtocol: Sendable { userAgent: String ) async throws -> Data func addSignature(signature: Data, containerFile: URL) async throws -> ContainerWrapperProtocol + @discardableResult func extendSignatureToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol + @discardableResult func extendSignaturesToLTA(containerFile: URL) async throws -> ContainerWrapperProtocol } extension ContainerWrapperProtocol { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift index 5cb616cc..d8586404 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Domain/Models/SignatureWrapper.swift @@ -52,6 +52,13 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { public var status: SignatureStatus public var diagnosticsInfo: String + public var archiveTimestampTime: String + public var archiveTimestampCert: Data + + public var isLTAExtended: Bool { + !archiveTimestampCert.isEmpty + } + public init(pos: Int, signingCert: Data, timestampCert: Data, @@ -71,7 +78,9 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { status: SignatureStatus = .unknown, format: String, messageImprint: Data, - diagnosticsInfo: String) { + diagnosticsInfo: String, + archiveTimestampTime: String = "", + archiveTimestampCert: Data = Data()) { self.pos = pos self.signingCert = signingCert self.timestampCert = timestampCert @@ -92,5 +101,7 @@ public struct SignatureWrapper: Sendable, Identifiable, Hashable { self.format = format self.messageImprint = messageImprint self.diagnosticsInfo = diagnosticsInfo + self.archiveTimestampTime = archiveTimestampTime + self.archiveTimestampCert = archiveTimestampCert } } diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift index a99ecdcd..0da82e19 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/Errors/DigiDocError.swift @@ -31,6 +31,7 @@ public enum DigiDocError: Error { case signatureRemovingFailed(ErrorDetail) case dataFileRemovingFailed(ErrorDetail) case signatureAddingFailed(ErrorDetail) + case signatureExtensionFailed(ErrorDetail) public var errorDetail: ErrorDetail { switch self { @@ -43,7 +44,8 @@ public enum DigiDocError: Error { .containerDataFileSavingFailed(let errorDetail), .signatureRemovingFailed(let errorDetail), .dataFileRemovingFailed(let errorDetail), - .signatureAddingFailed(let errorDetail): + .signatureAddingFailed(let errorDetail), + .signatureExtensionFailed(let errorDetail): return errorDetail case .alreadyInitialized: diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift index e9056715..5fde2bb7 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainer.swift @@ -291,6 +291,45 @@ public actor SignedContainer: SignedContainerProtocol, Loggable { containerUtil: containerUtil ) } + + @discardableResult + public func extendSignature() async throws -> SignedContainerProtocol { + guard let containerFile else { + throw DigiDocError.signatureExtensionFailed( + ErrorDetail(message: "Cannot extend signature: container file is nil", code: 0) + ) + } + let containerWrapper = try await container.extendSignatureToLTA(containerFile: containerFile) + return SignedContainer( + containerFile: containerFile, + isExistingContainer: true, + container: containerWrapper, + timestamps: timestamps, + fileManager: fileManager, + containerUtil: containerUtil + ) + } + + @discardableResult + public func extendSignatures() async throws -> SignedContainerProtocol { + guard let containerFile else { + SignedContainer.logger().error("Cannot extend signatures: container file is nil") + throw DigiDocError.signatureExtensionFailed( + ErrorDetail(message: "Cannot extend signatures: container file is nil", code: 0) + ) + } + SignedContainer.logger().info("Extending signatures to LTA") + let containerWrapper = try await container.extendSignaturesToLTA(containerFile: containerFile) + let extendedContainerFile = await containerWrapper.getContainerURL() + return SignedContainer( + containerFile: extendedContainerFile, + isExistingContainer: true, + container: containerWrapper, + timestamps: timestamps, + fileManager: fileManager, + containerUtil: containerUtil + ) + } } extension SignedContainer { diff --git a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift index a19741d4..f1b35d28 100644 --- a/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift +++ b/Modules/LibdigidocLib/Sources/LibdigidocSwift/SignedContainerProtocol.swift @@ -52,10 +52,19 @@ public protocol SignedContainerProtocol: GeneralContainer, Sendable { userAgent: String ) async throws -> Data func addSignature(signature: Data, containerFile: URL) async throws -> SignedContainerProtocol + @discardableResult func extendSignature() async throws -> SignedContainerProtocol + @discardableResult func extendSignatures() async throws -> SignedContainerProtocol } extension SignedContainerProtocol { func saveDataFile(dataFile: DataFileWrapper) async throws -> URL { try await saveDataFile(dataFile: dataFile, to: nil) } + + public func extendSignature(_ enabled: Bool) async throws -> any SignedContainerProtocol { + guard enabled else { return self } + let mimetype = await getContainerMimetype() + guard mimetype != CommonsLib.Constants.MimeType.Ddoc else { return self } + return try await extendSignature() + } } diff --git a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift index 4ee7cab9..01e97c48 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift @@ -287,49 +287,29 @@ extension URL { return [] } - public func isAsicContainer( - fileUtil: FileUtilProtocol = fileUtil(), - ) async -> Bool { - do { - if try isZipFile(), - try await fileUtil.getFileFromZipFile(from: self, fileNameToFind: "mimetype") != nil { - return true - } - } catch { - return false - } + public func isAsicContainer(fileUtil: FileUtilProtocol = fileUtil()) async -> Bool { + await containsZipEntry(named: "mimetype", fileUtil: fileUtil) + } - return false + public func isCades(fileUtil: FileUtilProtocol = fileUtil()) async -> Bool { + await containsZipEntry(named: "p7s", fileUtil: fileUtil) } - public func isCades( - fileUtil: FileUtilProtocol = fileUtil(), - ) async -> Bool { - do { - if try isZipFile(), - try await fileUtil.getFileFromZipFile(from: self, fileNameToFind: "p7s") != nil { - return true - } - } catch { - return false - } + public func isXades(fileUtil: FileUtilProtocol = fileUtil()) async -> Bool { + await containsZipEntry(named: "signatures.xml", fileUtil: fileUtil) + } - return false + public func containsDdoc(fileUtil: FileUtilProtocol = fileUtil()) async -> Bool { + await containsZipEntry(named: ".\(Constants.Extension.Ddoc)", fileUtil: fileUtil) } - public func isXades( - fileUtil: FileUtilProtocol = fileUtil() - ) async -> Bool { + private func containsZipEntry(named name: String, fileUtil: FileUtilProtocol) async -> Bool { do { - if try isZipFile(), - try await fileUtil.getFileFromZipFile(from: self, fileNameToFind: "signatures.xml") != nil { - return true - } + guard try isZipFile() else { return false } + return try await fileUtil.getFileFromZipFile(from: self, fileNameToFind: name) != nil } catch { return false } - - return false } // Check if file is zip format diff --git a/Modules/UtilsLib/Tests/UtilsLibTests/Extensions/URLExtensionsTests.swift b/Modules/UtilsLib/Tests/UtilsLibTests/Extensions/URLExtensionsTests.swift index 398973c3..fe4c84f8 100644 --- a/Modules/UtilsLib/Tests/UtilsLibTests/Extensions/URLExtensionsTests.swift +++ b/Modules/UtilsLib/Tests/UtilsLibTests/Extensions/URLExtensionsTests.swift @@ -464,6 +464,40 @@ struct URLExtensionsTests { #expect(!isXades) } + @Test + func containsDdoc_successReturningTrue() async throws { + let mockFile = URL(fileURLWithPath: "/mock/path/wrapped.ddoc") + + let mockContainer = try TestContainerUtil.createMockContainer( + with: [mockFile.lastPathComponent: "Test content"], + containerExtension: "asics") + + mockFileUtil.getFileFromZipFileHandler = { _, _ in + return mockFile + } + + defer { + try? FileManager.default.removeItem(at: mockContainer) + } + + let containsDdoc = await mockContainer.containsDdoc(fileUtil: mockFileUtil) + + #expect(containsDdoc) + } + + @Test + func containsDdoc_successReturningFalse() async { + let mockContainer = URL(fileURLWithPath: "/mock/path/regularContainer.asics") + + mockFileUtil.getFileFromZipFileHandler = { _, _ in + return nil + } + + let containsDdoc = await mockContainer.containsDdoc(fileUtil: mockFileUtil) + + #expect(!containsDdoc) + } + private func createTestPDF(at url: URL) -> URL { var pageSize = CGRect(x: 0, y: 0, width: 100, height: 100) diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index ebdf4655..43ab69f6 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -357,6 +357,15 @@ extension Container { } } + @MainActor + var ltaSettingsViewModel: Factory { + self { @MainActor in + LTASettingsViewModel( + dataStore: self.dataStore() + ) + } + } + var signatureUtil: Factory { self { SignatureUtil() } } diff --git a/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift b/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift index 1570e705..98bed9c5 100644 --- a/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift +++ b/RIADigiDoc/Domain/Model/Settings/SigningServicesSettingsViewTab.swift @@ -20,4 +20,5 @@ enum SigningServicesSettingsViewTab: Int, Sendable { case timestampServices = 0 case mobileIdAndSmartId = 1 + case lta = 2 } diff --git a/RIADigiDoc/Domain/Preferences/DataStore.swift b/RIADigiDoc/Domain/Preferences/DataStore.swift index 185ad081..daf6b1fb 100644 --- a/RIADigiDoc/Domain/Preferences/DataStore.swift +++ b/RIADigiDoc/Domain/Preferences/DataStore.swift @@ -505,6 +505,16 @@ public actor DataStore: DataStoreProtocol { userDefaults().set(isAlwaysEnabled, forKey: Keys.isCrashlyticsAlwaysEnabled) } + // MARK: - LTA Settings + + public func getIsDefaultLTAEnabled() async -> Bool { + userDefaults().bool(forKey: Keys.isDefaultLTAEnabled) + } + + public func setIsDefaultLTAEnabled(_ isEnabled: Bool) async { + userDefaults().set(isEnabled, forKey: Keys.isDefaultLTAEnabled) + } + // MARK: - Migration public func getIsRecentDocumentsMigrationDone() async -> Bool { userDefaults().bool(forKey: Keys.isRecentDocumentsMigrationDone) @@ -581,5 +591,6 @@ public actor DataStore: DataStoreProtocol { static let isLogFileSaved = "isLogFileSaved" static let isCrashlyticsAlwaysEnabled = "isCrashlyticsAlwaysEnabled" static let isRecentDocumentsMigrationDone = "isRecentDocumentsMigrationDone" + static let isDefaultLTAEnabled = "isDefaultLTAEnabled" } } diff --git a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift index c0591299..06f0dd51 100644 --- a/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift +++ b/RIADigiDoc/Domain/Preferences/DataStoreProtocol.swift @@ -123,6 +123,10 @@ public protocol DataStoreProtocol: Sendable { func getIsCrashlyticsAlwaysEnabled() async -> Bool func setIsCrashlyticsAlwaysEnabled(_ isEnabled: Bool) async + // MARK: - LTA Settings + func getIsDefaultLTAEnabled() async -> Bool + func setIsDefaultLTAEnabled(_ isEnabled: Bool) async + // MARK: - Migration func getIsRecentDocumentsMigrationDone() async -> Bool func setIsRecentDocumentsMigrationDone(_ isDone: Bool) async diff --git a/RIADigiDoc/Domain/Repository/Siva/SivaRepositoryProtocol.swift b/RIADigiDoc/Domain/Repository/Siva/SivaRepositoryProtocol.swift index 4144765b..98a1fc56 100644 --- a/RIADigiDoc/Domain/Repository/Siva/SivaRepositoryProtocol.swift +++ b/RIADigiDoc/Domain/Repository/Siva/SivaRepositoryProtocol.swift @@ -28,3 +28,14 @@ public protocol SivaRepositoryProtocol: Sendable { parentContainer: SignedContainerProtocol ) async throws -> SignedContainerProtocol } + +extension SivaRepositoryProtocol { + func shouldOpenNestedTimestampedContainer(_ container: SignedContainerProtocol) async -> Bool { + guard await isTimestampedContainer(signedContainer: container) else { return false } + let isCades = await container.isCades() + let isXades = await container.isXades() + guard !isCades, !isXades else { return false } + guard let containerFile = await container.getRawContainerFile() else { return false } + return await isSivaConfirmationNeeded(files: [containerFile]) + } +} diff --git a/RIADigiDoc/Domain/Service/Siva/SivaService.swift b/RIADigiDoc/Domain/Service/Siva/SivaService.swift index 5d17f025..73a99b8e 100644 --- a/RIADigiDoc/Domain/Service/Siva/SivaService.swift +++ b/RIADigiDoc/Domain/Service/Siva/SivaService.swift @@ -54,6 +54,10 @@ actor SivaService: SivaServiceProtocol { let isCades = await file.isCades(fileUtil: fileUtil) let isXades = await file.isXades(fileUtil: fileUtil) + if mimetype == Constants.MimeType.Asics { + return await file.containsDdoc(fileUtil: fileUtil) || isCades + } + return Constants.MimeType.SivaContainers.contains(mimetype) && !isXades || ( Constants.MimeType.Pdf == mimetype && file.isSignedPDF() ) || isCades diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index 03b639f8..6a3edf35 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -127,6 +127,78 @@ } } }, + "Archive timestamp" : { + "comment" : "Archive timestamp label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive timestamp" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi ajatempel" + } + } + } + }, + "Archive timestamp valid until" : { + "comment" : "Archive timestamp valid until badge text", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive timestamp valid until: %@" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi ajatempel kehtib kuni: %@" + } + } + } + }, + "Archive TS Certificate" : { + "comment" : "Archive TS certificate label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive TS Certificate" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi TS sertifikaat" + } + } + } + }, + "Archive TS Certificate issuer" : { + "comment" : "Archive TS certificate issuer label in signature details", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Archive TS Certificate issuer" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiivi TS sertifikaadi väljaandja" + } + } + } + }, "Authentication certificate" : { "comment" : "My eID PIN 1 title", "extractionState" : "manual", @@ -1691,6 +1763,114 @@ } } }, + "Extend" : { + "comment" : "Confirm button label in extend signatures dialog", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extend" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikenda" + } + } + } + }, + "Extend signatures" : { + "comment" : "Button label for extending signatures to LTA", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Extend signatures" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pikenda allkirju" + } + } + } + }, + "Extend signatures confirm message" : { + "comment" : "Confirmation dialog message shown before extending signatures to LTA", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "All signatures in the container will be extended to LTA format." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kõik ümbriku allkirjad pikendatakse LTA formaati." + } + } + } + }, + "Extending signatures failed" : { + "comment" : "Shown as Toast when extending signatures to LTA fails", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to extend signature(s)" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allkirja(de) pikendamine ebaõnnestus" + } + } + } + }, + "Container validity cannot be extended" : { + "comment" : "Dialog title when a BDOC/DDOC could not be extended and was wrapped into a timestamped ASiC-S", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Container validity cannot be extended" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Konteineri kehtivust ei saa pikendada" + } + } + } + }, + "Failed to extend signatures and wrapped to timestamped container." : { + "comment" : "Dialog message when a BDOC/DDOC could not be extended and was wrapped into a timestamped ASiC-S", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to extend signatures and wrapped to timestamped container." + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allkirjade pikendamine ebaõnnestus ja konteiner pakiti ajatempliga konteinerisse." + } + } + } + }, "Failed mobile-ID transaction" : { "comment" : "Mobile-ID error for signatureHashMismatch", "extractionState" : "manual", @@ -4128,6 +4308,42 @@ } } }, + "Main settings default lta disabled" : { + "comment" : "Main settings default lta disabled", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do not use Long Term Archival (LTA) format when signing" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ei kasuta allkirjastamisel arhiveerimise ajatempli (LTA) vormingut" + } + } + } + }, + "Main settings default lta title" : { + "comment" : "Main settings default lta title", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Use Long Term Archival (LTA) format when signing" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kasuta allkirjastamisel arhiveerimise ajatempli (LTA) vormingut" + } + } + } + }, "Main settings default manual access title" : { "comment" : "Main settings default manual access title", "extractionState" : "manual", @@ -4164,6 +4380,24 @@ } } }, + "Main settings lta tab title" : { + "comment" : "Tab title for LTA settings in Signing services", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Long Term Archival (LTA)" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Arhiveerimise ajatempel (LTA)" + } + } + } + }, "Main settings menu advanced" : { "comment" : "Main settings menu advanced", "extractionState" : "manual", @@ -7290,6 +7524,24 @@ } } }, + "Signatures extended" : { + "comment" : "Shown as Toast when extending signatures to LTA succeeds", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Signature(s) extended successfully" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Allkirja(de) pikendamine õnnestus" + } + } + } + }, "Signed at" : { "comment" : "Signed date and time", "extractionState" : "manual", @@ -7866,6 +8118,60 @@ } } }, + "Timestamp is invalid" : { + "comment" : "Timestamp invalid status", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timestamp is invalid" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajatempel ei ole kehtiv" + } + } + } + }, + "Timestamp is unknown" : { + "comment" : "Timestamp unknown status", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timestamp is unknown" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajatempel on teadmata" + } + } + } + }, + "Timestamp is valid" : { + "comment" : "Timestamp valid status", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Timestamp is valid" + } + }, + "et" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ajatempel on kehtiv" + } + } + } + }, "Too many requests" : { "comment" : "Signing error when request responds with too many requests error", "extractionState" : "manual", diff --git a/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift b/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift index 832fbd78..0ae11f50 100644 --- a/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift +++ b/RIADigiDoc/UI/Component/Bottom Sheet/ContainerNameBottomSheetActions.swift @@ -24,10 +24,12 @@ struct ContainerNameBottomSheetActions { isSaveButtonShown: Bool = true, isSignButtonShown: Bool, isEncryptButtonShown: Bool, + isExtendSignaturesButtonShown: Bool, onRenameContainerButtonClick: @escaping () -> Void, onSaveContainerButtonClick: @escaping () -> Void, onSignContainerButtonClick: @escaping () -> Void, - onEncryptContainerButtonClick: @escaping () -> Void + onEncryptContainerButtonClick: @escaping () -> Void, + onExtendSignaturesClick: @escaping () -> Void ) -> [BottomSheetButton] { [ BottomSheetButton( @@ -59,6 +61,14 @@ struct ContainerNameBottomSheetActions { accessibilityLabel: "Sign document", showExtraIcon: true, onClick: onSignContainerButtonClick + ), + BottomSheetButton( + showButton: isExtendSignaturesButtonShown, + icon: "ic_m3_more_time_48pt_wght400", + title: "Extend signatures", + accessibilityLabel: "Extend signatures", + showExtraIcon: false, + onClick: onExtendSignaturesClick ) ] } diff --git a/RIADigiDoc/UI/Component/Container/ContainerNameView.swift b/RIADigiDoc/UI/Component/Container/ContainerNameView.swift index 65a52ef8..158d8b4e 100644 --- a/RIADigiDoc/UI/Component/Container/ContainerNameView.swift +++ b/RIADigiDoc/UI/Component/Container/ContainerNameView.swift @@ -41,6 +41,7 @@ struct ContainerNameView: View { let isSaveButtonShown: Bool let isSignButtonShown: Bool let isEncryptButtonShown: Bool + let isExtendSignaturesButtonShown: Bool let showLeftActionButton: Bool let showRightActionButton: Bool let leftActionButtonName: String @@ -53,6 +54,7 @@ struct ContainerNameView: View { let onRenameContainerButtonClick: () -> Void let onSignContainerButtonClick: () -> Void let onEncryptContainerButtonClick: () -> Void + let onExtendSignaturesClick: () -> Void private var bottomSheetActions: [BottomSheetButton] { ContainerNameBottomSheetActions.actions( @@ -60,10 +62,12 @@ struct ContainerNameView: View { isSaveButtonShown: isSaveButtonShown, isSignButtonShown: isSignButtonShown, isEncryptButtonShown: isEncryptButtonShown, + isExtendSignaturesButtonShown: isExtendSignaturesButtonShown, onRenameContainerButtonClick: onRenameContainerButtonClick, onSaveContainerButtonClick: onSaveContainerButtonClick, onSignContainerButtonClick: onSignContainerButtonClick, - onEncryptContainerButtonClick: onEncryptContainerButtonClick + onEncryptContainerButtonClick: onEncryptContainerButtonClick, + onExtendSignaturesClick: onExtendSignaturesClick ) } @@ -169,6 +173,7 @@ struct ContainerNameView: View { isSaveButtonShown: true, isSignButtonShown: true, isEncryptButtonShown: false, + isExtendSignaturesButtonShown: true, showLeftActionButton: true, showRightActionButton: true, leftActionButtonName: "Sign", @@ -180,7 +185,8 @@ struct ContainerNameView: View { onSaveContainerButtonClick: {}, onRenameContainerButtonClick: {}, onSignContainerButtonClick: {}, - onEncryptContainerButtonClick: {} + onEncryptContainerButtonClick: {}, + onExtendSignaturesClick: {} ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) diff --git a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesListView.swift b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesListView.swift index 09e3f15b..b9398294 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesListView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesListView.swift @@ -41,6 +41,9 @@ struct CryptoDataFilesListView: View { LazyVStack { if #available(iOS 26.0, *) { ForEach(dataFiles.enumerated(), id: \.offset) { index, dataFile in + if index > 0 { + Divider() + } CryptoDataFilesView( fileIndex: index + 1, onOpenFileButtonClick: onOpenFileButtonClick, @@ -58,6 +61,9 @@ struct CryptoDataFilesListView: View { } } else { ForEach(Array(dataFiles.enumerated()), id: \.offset) { index, dataFile in + if index > 0 { + Divider() + } CryptoDataFilesView( fileIndex: index + 1, onOpenFileButtonClick: onOpenFileButtonClick, diff --git a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift index 6af017a3..d7a2de10 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift @@ -22,7 +22,6 @@ import FactoryKit import LibdigidocLibSwift struct CryptoDataFilesSection: View { - @Environment(\.openURL) private var openURL @Environment(LanguageSettings.self) private var languageSettings @State private var viewModel: EncryptViewModel @@ -60,14 +59,6 @@ struct CryptoDataFilesSection: View { self._navigateToNestedSignedContainerView = navigateToNestedSignedContainerView } - private var sivaMessage: String { - languageSettings.localized("Siva message") - } - - private var sivaMessageUrl: String { - languageSettings.localized("Siva message url") - } - var body: some View { CryptoDataFilesListView( dataFiles: viewModel.dataFiles, @@ -82,7 +73,9 @@ struct CryptoDataFilesSection: View { showRemoveDataFileModal = true } ) - .alert(sivaMessage, isPresented: $showSivaMessage, actions: alertActions) + .sivaConfirmationAlert(isPresented: $showSivaMessage) { confirmed in + confirmSiva(confirmed) + } .background(fileSaverBackground) .filePreview(item: $viewModel.previewFile) } @@ -122,22 +115,6 @@ struct CryptoDataFilesSection: View { ) } - @ViewBuilder private func alertActions() -> some View { - Button(languageSettings.localized("OK")) { - confirmSiva(true) - } - Button(languageSettings.localized("Cancel")) { - confirmSiva(false) - } - Button(languageSettings.localized("Read more here")) { - if let url = URL(string: sivaMessageUrl), - UIApplication.shared.canOpenURL(url) { - openURL(url) - } - showSivaMessage = false - } - } - private func confirmSiva(_ confirmed: Bool) { guard let dataFile = selectedDataFile else { showSivaMessage = false diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index cafd2b14..2af43f33 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -211,6 +211,7 @@ struct EncryptView: View { isSaveButtonShown: viewModel.isContainerEncrypted || viewModel.isContainerDecrypted, isSignButtonShown: viewModel.isSignButtonShown, isEncryptButtonShown: false, + isExtendSignaturesButtonShown: false, showLeftActionButton: false, showRightActionButton: viewModel.isEncryptButtonShown || viewModel.isDecryptButtonShown, @@ -262,6 +263,9 @@ struct EncryptView: View { }, onEncryptContainerButtonClick: { // Do nothing + }, + onExtendSignaturesClick: { + // Do nothing } ) .background( diff --git a/RIADigiDoc/UI/Component/Container/DataFilesListView.swift b/RIADigiDoc/UI/Component/Container/DataFilesListView.swift index d6f88baf..738f0bb0 100644 --- a/RIADigiDoc/UI/Component/Container/DataFilesListView.swift +++ b/RIADigiDoc/UI/Component/Container/DataFilesListView.swift @@ -39,6 +39,9 @@ struct DataFilesListView: View { LazyVStack { if #available(iOS 26.0, *) { ForEach(dataFiles.enumerated(), id: \.offset) { index, dataFile in + if index > 0 { + Divider() + } DataFilesView( fileIndex: index + 1, onOpenFileButtonClick: onOpenFileButtonClick, @@ -54,6 +57,9 @@ struct DataFilesListView: View { } } else { ForEach(Array(dataFiles.enumerated()), id: \.offset) { index, dataFile in + if index > 0 { + Divider() + } DataFilesView( fileIndex: index + 1, onOpenFileButtonClick: onOpenFileButtonClick, diff --git a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift index 42d2f8fd..6e4a0425 100644 --- a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift @@ -22,7 +22,6 @@ import FactoryKit import LibdigidocLibSwift struct DataFilesSection: View { - @Environment(\.openURL) private var openURL @Environment(LanguageSettings.self) private var languageSettings @State private var viewModel: SigningViewModel @@ -34,14 +33,6 @@ struct DataFilesSection: View { @Binding var showRemoveDataFileModal: Bool @Binding var navigateToNestedCryptoContainerView: Bool - private var sivaMessage: String { - languageSettings.localized("Siva message") - } - - private var sivaMessageUrl: String { - languageSettings.localized("Siva message url") - } - init( viewModel: SigningViewModel, isContainerSigned: Bool, @@ -74,7 +65,9 @@ struct DataFilesSection: View { showRemoveDataFileModal = true } ) - .alert(sivaMessage, isPresented: $showSivaMessage, actions: alertActions) + .sivaConfirmationAlert(isPresented: $showSivaMessage) { confirmed in + confirmSiva(confirmed) + } .background(fileSaverBackground) .filePreview(item: $viewModel.previewFile) } @@ -114,22 +107,6 @@ struct DataFilesSection: View { ) } - @ViewBuilder private func alertActions() -> some View { - Button(languageSettings.localized("OK")) { - confirmSiva(true) - } - Button(languageSettings.localized("Cancel")) { - confirmSiva(false) - } - Button(languageSettings.localized("Read more here")) { - if let url = URL(string: sivaMessageUrl), - UIApplication.shared.canOpenURL(url) { - openURL(url) - } - showSivaMessage = false - } - } - private func confirmSiva(_ confirmed: Bool) { guard let dataFile = selectedDataFile else { showSivaMessage = false diff --git a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift index 5ec90543..c828f35b 100644 --- a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift @@ -117,6 +117,14 @@ struct SignatureDetailView: View { return "\(mobileTime.date) \(mobileTime.time)" } + var archiveTimestampTime: String { + let timestamp = DateUtil.getFormattedDateTime( + dateTimeString: signature.archiveTimestampTime, + isUTC: false + ) + return "\(timestamp.date) \(timestamp.time)" + } + init( certificateDetailViewModel: CertificateDetailViewModel = Container.shared.certificateDetailViewModel(), signature: SignatureWrapper, @@ -177,7 +185,7 @@ struct SignatureDetailView: View { selectedTab: $selectedTab, titles: [signersRoleAndAddressTitle, signersDetailsTitle], content: { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: Dimensions.Padding.SPadding) { if selectedTab == .roleAndAddress { RoleDetailsView(signature: signature) } else { @@ -323,8 +331,41 @@ struct SignatureDetailView: View { value: signersMobileTime ) ) + + if !isTimestamp && signature.isLTAExtended { + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive timestamp"), + value: archiveTimestampTime + ) + ) + + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive TS Certificate issuer"), + value: viewModel.getIssuerName(cert: signature.archiveTimestampCert) + ) + ) + + NavigationLink( + value: NavigationDestination + .certificateDetailView(certificate: signature.archiveTimestampCert) + ) { + SignerDetailView( + signatureDataItem: SignatureDataItem( + title: languageSettings.localized("Archive TS Certificate"), + value: viewModel.getSubjectName( + cert: signature.archiveTimestampCert + ), + extraIcon: "ic_m3_expand_content_48pt_wght400", + ) + ) + } + .buttonStyle(.plain) + } } } + .frame(maxWidth: .infinity, alignment: .leading) }) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index 22dc6389..ebc13a49 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -102,6 +102,10 @@ struct NFCView: View { languageSettings.localized("Signature added") } + private var extendingSignaturesFailedMessage: String { + languageSettings.localized("Extending signatures failed") + } + let signedContainer: SignedContainerProtocol? let cryptoContainer: CryptoContainerProtocol? @@ -371,10 +375,16 @@ struct NFCView: View { return } - Toast.show(signatureAddedMessage, type: .success) - - if voiceOverEnabled { - AccessibilityUtil.announceMessage(signatureAddedMessage) + if viewModel.signatureExtensionFailed { + Toast.show(extendingSignaturesFailedMessage, type: .error) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(extendingSignaturesFailedMessage) + } + } else { + Toast.show(signatureAddedMessage, type: .success) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } } onSuccess(container) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift b/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift index 73ad7e49..1a9bd68d 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SignatureView.swift @@ -42,6 +42,7 @@ struct SignatureView: View { let isTimestamp: Bool let nameUtil: NameUtilProtocol let signatureUtil: SignatureUtilProtocol + let certificateUtil: CertificateUtilProtocol let showSignedDate: Bool let showMoreOptionsButton: Bool let showRole: Bool @@ -84,6 +85,7 @@ struct SignatureView: View { isTimestamp: Bool = false, nameUtil: NameUtilProtocol = Container.shared.nameUtil(), signatureUtil: SignatureUtilProtocol = Container.shared.signatureUtil(), + certificateUtil: CertificateUtilProtocol = Container.shared.certificateUtil(), showSignedDate: Bool = true, showMoreOptionsButton: Bool = true, showRole: Bool = true, @@ -98,6 +100,7 @@ struct SignatureView: View { self.isTimestamp = isTimestamp self.nameUtil = nameUtil self.signatureUtil = signatureUtil + self.certificateUtil = certificateUtil self.showSignedDate = showSignedDate self.showMoreOptionsButton = showMoreOptionsButton self.showRole = showRole @@ -106,6 +109,14 @@ struct SignatureView: View { self.onSelect = onSelect } + private var archiveTimestampInfo: (text: String, isExpired: Bool)? { + guard !isTimestamp && signature.isLTAExtended else { return nil } + guard let date = certificateUtil.getNotValidAfterDate(cert: signature.archiveTimestampCert) else { return nil } + let formatted = DateUtil.getFormattedDateTime(date: date, isUTC: false) + let text = languageSettings.localized("Archive timestamp valid until", [formatted.date]) + return (text: text, isExpired: date < Date()) + } + var body: some View { let signedDate = DateUtil.getFormattedDateTime( dateTimeString: signature.trustedSigningTime, @@ -145,9 +156,11 @@ struct SignatureView: View { ColoredSignedStatusText( text: languageSettings.localized( - signatureUtil.getSignatureStatusText(status: signature.status) + signatureUtil.getSignatureStatusText(status: signature.status, isTimestamp: isTimestamp) ), - status: signature.status + status: signature.status, + archiveTimestampText: archiveTimestampInfo?.text, + isArchiveTimestampExpired: archiveTimestampInfo?.isExpired ?? false ) .multilineTextAlignment(.center) diff --git a/RIADigiDoc/UI/Component/Container/Signing/SignerDetailView.swift b/RIADigiDoc/UI/Component/Container/Signing/SignerDetailView.swift index 163730de..75caee71 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SignerDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SignerDetailView.swift @@ -49,9 +49,9 @@ struct SignerDetailView: View { Text(verbatim: signatureDataItem.value) .foregroundStyle(theme.onSurface) .font(typography.bodyLarge) + .fixedSize(horizontal: false, vertical: true) } - - Spacer() + .frame(maxWidth: .infinity, alignment: .leading) if let extraIcon = signatureDataItem.extraIcon { Image(extraIcon) @@ -69,6 +69,7 @@ struct SignerDetailView: View { Divider() } + .padding(.horizontal, Dimensions.Padding.SPadding) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index 806f8a53..f84a5895 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -48,6 +48,7 @@ struct SigningView: View { @State private var showRenameModal = false @State private var showRemoveSignatureModal = false @State private var showRemoveDataFileModal = false + @State private var showExtendSignaturesModal = false @State private var isImportingAddedFiles: Bool = false @State private var showingShareSheet = false @@ -143,6 +144,14 @@ struct SigningView: View { viewModel.isNestedContainer() } + private var isExtendSignaturesButtonShown: Bool { + isSignedContainer && !isNestedContainer + } + + private var extendSignaturesLabel: String { + languageSettings.localized("Extend signatures") + } + private var containerName: String { URL(fileURLWithPath: viewModel.containerName) .deletingPathExtension() @@ -200,6 +209,7 @@ struct SigningView: View { isSaveButtonShown: true, isSignButtonShown: false, isEncryptButtonShown: !isContainerSigned && !isNestedContainer, + isExtendSignaturesButtonShown: isExtendSignaturesButtonShown, showLeftActionButton: isContainerSigned && isSignButtonShown, showRightActionButton: isContainerSigned && !isNestedContainer, leftActionButtonName: languageSettings.localized("Add signature"), @@ -237,6 +247,9 @@ struct SigningView: View { Task { await convertToCryptoContainer() } + }, + onExtendSignaturesClick: { + showExtendSignaturesModal = true } ) .background( @@ -458,9 +471,44 @@ struct SigningView: View { onCancel: { showRemoveDataFileModal = false } ) } + + if showExtendSignaturesModal { + ConfirmModalView( + title: languageSettings.localized("Extend signatures"), + message: languageSettings.localized("Extend signatures confirm message"), + confirmButtonTitle: languageSettings.localized("Extend"), + onConfirm: { + showExtendSignaturesModal = false + Task { + await viewModel.extendSignatures() + } + }, + onCancel: { showExtendSignaturesModal = false } + ) + } } .animation(.easeInOut, value: showRenameModal) .animation(.easeInOut, value: showRemoveSignatureModal) + .animation(.easeInOut, value: showExtendSignaturesModal) + .sivaConfirmationAlert( + isPresented: $viewModel.showExtendSivaConfirmation, + onConfirm: { confirmed in + Task { await viewModel.openExtendedContainer(isSivaConfirmed: confirmed) } + }, + onReadMore: { + Task { await viewModel.openExtendedContainer(isSivaConfirmed: false) } + } + ) + .alert( + languageSettings.localized("Container validity cannot be extended"), + isPresented: $viewModel.showCannotExtendContainerDialog + ) { + Button(languageSettings.localized("OK")) {} + } message: { + Text(verbatim: languageSettings.localized( + "Failed to extend signatures and wrapped to timestamped container." + )) + } .onChange(of: viewModel.errorMessage) { _, error in guard let error else { return } let localizedMessage = languageSettings.localized(error.key, [error.args.joined(separator: ", ")]) @@ -481,6 +529,10 @@ struct SigningView: View { AccessibilityUtil.announceMessage(localizedMessage) } + if message.key == "Signatures extended" { + selectedTab = .signatures + } + viewModel.resetSuccessMessage() } .onChange(of: viewModel.navigateToNestedCryptoContainerView) { _, isNavigating in diff --git a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift index aa456aa4..7b43fa03 100644 --- a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift +++ b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift @@ -23,7 +23,6 @@ import LibdigidocLibSwift struct CryptoFileOpeningView: View { @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled - @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -32,16 +31,6 @@ struct CryptoFileOpeningView: View { @Binding var isFileOpeningLoading: Bool @Binding var isNavigatingToNextView: Bool - @State private var showSivaMessage = false - - private var sivaMessage: String { - languageSettings.localized("Siva message") - } - - private var sivaMessageUrl: String { - languageSettings.localized("Siva message url") - } - @State private var fileHandlingTask: Task? init( @@ -95,7 +84,9 @@ struct CryptoFileOpeningView: View { } } } else { - let localizedMessage = languageSettings.localized(errorMessage?.key ?? "General error", errorMessage?.args ?? []) + let localizedMessage = languageSettings.localized( + errorMessage?.key ?? "General error", errorMessage?.args ?? [] + ) Toast.show(localizedMessage) if voiceOverEnabled { diff --git a/RIADigiDoc/UI/Component/FileOpeningView.swift b/RIADigiDoc/UI/Component/FileOpeningView.swift index 115c5db1..c6372111 100644 --- a/RIADigiDoc/UI/Component/FileOpeningView.swift +++ b/RIADigiDoc/UI/Component/FileOpeningView.swift @@ -23,7 +23,6 @@ import LibdigidocLibSwift struct FileOpeningView: View { @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled - @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -35,14 +34,6 @@ struct FileOpeningView: View { @State private var showSivaMessage = false - private var sivaMessage: String { - languageSettings.localized("Siva message") - } - - private var sivaMessageUrl: String { - languageSettings.localized("Siva message url") - } - private var isErrorShown: Bool { guard let errorMessage = viewModel.errorMessage else { return false } return !errorMessage.key.isEmpty @@ -70,27 +61,20 @@ struct FileOpeningView: View { .onDisappear { fileHandlingTask?.cancel() } - .alert(sivaMessage, isPresented: $showSivaMessage) { - Button(languageSettings.localized("OK")) { + .sivaConfirmationAlert( + isPresented: $showSivaMessage, + onConfirm: { confirmed in Task { - await viewModel.handleSivaConfirmation() + if confirmed { + await viewModel.handleSivaConfirmation() + } else { + await viewModel.handleSivaCancellation() + } await handleFileOpening() } - } - Button(languageSettings.localized("Cancel")) { - Task { - await viewModel.handleSivaCancellation() - await handleFileOpening() - } - } - Button(languageSettings.localized("Read more here")) { - if let url = URL(string: sivaMessageUrl), - UIApplication.shared.canOpenURL(url) { - openURL(url) - } - dismiss() - } - } + }, + onReadMore: { dismiss() } + ) } } @@ -136,7 +120,9 @@ struct FileOpeningView: View { } } } else { - let localizedMessage = languageSettings.localized(errorMessage?.key ?? "General error", errorMessage?.args ?? []) + let localizedMessage = languageSettings.localized( + errorMessage?.key ?? "General error", errorMessage?.args ?? [] + ) Toast.show(localizedMessage) if voiceOverEnabled { diff --git a/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift b/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift index 752cf75f..5066e8ed 100644 --- a/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift +++ b/RIADigiDoc/UI/Component/Shared/ColoredSignedStatusText.swift @@ -26,6 +26,8 @@ struct ColoredSignedStatusText: View { let text: String let status: SignatureStatus + var archiveTimestampText: String? = nil + var isArchiveTimestampExpired: Bool = false private var isSignatureValidOrWarning: Bool { status == .valid || status == .warning || status == .nonQSCD @@ -49,12 +51,20 @@ struct ColoredSignedStatusText: View { } var body: some View { - TagBadge( - text: text, - tagBackgroundColor: tagBackgroundColor, - tagContentColor: tagContentColor, - additionalTextColor: additionalTextColor - ) + if let archiveText = archiveTimestampText { + TagBadge( + text: archiveText, + tagBackgroundColor: isArchiveTimestampExpired ? theme.errorContainer : theme.successContainer, + tagContentColor: isArchiveTimestampExpired ? theme.onErrorContainer : theme.onSuccessContainer + ) + } else { + TagBadge( + text: text, + tagBackgroundColor: tagBackgroundColor, + tagContentColor: tagContentColor, + additionalTextColor: additionalTextColor + ) + } } } @@ -73,4 +83,10 @@ struct ColoredSignedStatusText: View { text: "Signature is unknown", status: .unknown ) + + ColoredSignedStatusText( + text: "Signature is valid", + status: .valid, + archiveTimestampText: "Archive timestamp" + ) } diff --git a/RIADigiDoc/UI/Component/Shared/SivaConfirmationAlert.swift b/RIADigiDoc/UI/Component/Shared/SivaConfirmationAlert.swift new file mode 100644 index 00000000..ed55770d --- /dev/null +++ b/RIADigiDoc/UI/Component/Shared/SivaConfirmationAlert.swift @@ -0,0 +1,73 @@ +/* + * 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 + * + */ + +import SwiftUI + +extension View { + /// Presents the shared SiVa confirmation alert. `onConfirm(true)` maps to OK (send to SiVa), + /// `onConfirm(false)` to Cancel. `onReadMore` runs after opening the "read more" URL (e.g. to dismiss a view). + func sivaConfirmationAlert( + isPresented: Binding, + onConfirm: @escaping (Bool) -> Void, + onReadMore: (() -> Void)? = nil + ) -> some View { + modifier( + SivaConfirmationAlertModifier( + isPresented: isPresented, + onConfirm: onConfirm, + onReadMore: onReadMore + ) + ) + } +} + +private struct SivaConfirmationAlertModifier: ViewModifier { + @Binding var isPresented: Bool + let onConfirm: (Bool) -> Void + let onReadMore: (() -> Void)? + + @Environment(LanguageSettings.self) private var languageSettings + @Environment(\.openURL) private var openURL + + private var sivaMessage: String { + languageSettings.localized("Siva message") + } + + private var sivaMessageUrl: String { + languageSettings.localized("Siva message url") + } + + func body(content: Content) -> some View { + content.alert(sivaMessage, isPresented: $isPresented) { + Button(languageSettings.localized("OK")) { + onConfirm(true) + } + Button(languageSettings.localized("Cancel")) { + onConfirm(false) + } + Button(languageSettings.localized("Read more here")) { + if let url = URL(string: sivaMessageUrl), + UIApplication.shared.canOpenURL(url) { + openURL(url) + } + onReadMore?() + } + } + } +} diff --git a/RIADigiDoc/UI/Component/Shared/TabView.swift b/RIADigiDoc/UI/Component/Shared/TabView.swift index 1b474713..7d264914 100644 --- a/RIADigiDoc/UI/Component/Shared/TabView.swift +++ b/RIADigiDoc/UI/Component/Shared/TabView.swift @@ -89,6 +89,7 @@ struct TabView: View where Tab.RawValue == } .padding(.top, Dimensions.Padding.LPadding) content() + .padding(.top, Dimensions.Padding.SPadding) } } } diff --git a/RIADigiDoc/UI/Component/Shared/TagBadge.swift b/RIADigiDoc/UI/Component/Shared/TagBadge.swift index 20b8ff73..4a788b43 100644 --- a/RIADigiDoc/UI/Component/Shared/TagBadge.swift +++ b/RIADigiDoc/UI/Component/Shared/TagBadge.swift @@ -67,8 +67,9 @@ struct TagBadge: View { Text(parts[0]) .font(typography.bodyMedium) .lineLimit(nil) - .multilineTextAlignment(.center) + .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, Dimensions.Padding.XSPadding) .padding(.vertical, Dimensions.Padding.XXSPadding) .background(tagBackgroundColor) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift index 1b5b5c42..70af197a 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift @@ -43,15 +43,19 @@ struct SigningServicesSettingsView: View { selectedTab: $selectedTab, titles: [ languageSettings.localized("Main settings timestamp services title"), - languageSettings.localized("Main settings mobile id and smart id title") + languageSettings.localized("Main settings mobile id and smart id title"), + languageSettings.localized("Main settings lta tab title") ], content: { if selectedTab == .timestampServices { TimeStampSettingsView() .padding(.horizontal, Dimensions.Padding.SPadding) - } else { + } else if selectedTab == .mobileIdAndSmartId { MobileIDSmartIDSettingsView() .padding(.horizontal, Dimensions.Padding.SPadding) + } else { + LTASettingsView() + .padding(.horizontal, Dimensions.Padding.SPadding) } } ) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift new file mode 100644 index 00000000..f70df2e6 --- /dev/null +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView/LTASettingsView.swift @@ -0,0 +1,70 @@ +/* + * 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 + * + */ + +import SwiftUI +import FactoryKit + +struct LTASettingsView: View { + @Environment(LanguageSettings.self) private var languageSettings + + @State private var viewModel: LTASettingsViewModel + + init() { + _viewModel = State(wrappedValue: Container.shared.ltaSettingsViewModel()) + } + + var body: some View { + AdvancedSettingsSectionColumn( + title: languageSettings.localized("Main settings lta tab title"), + isScrollable: false + ) { + OutlinedRadioButtonCard( + title: languageSettings.localized("Main settings default lta disabled"), + isSelected: !viewModel.isDefaultLTAEnabled, + onSelect: { + viewModel.isDefaultLTAEnabled = false + }, + accessibilityInputLabel: .disabledSetting + ) + OutlinedRadioButtonCard( + title: languageSettings.localized("Main settings default lta title"), + isSelected: viewModel.isDefaultLTAEnabled, + onSelect: { + viewModel.isDefaultLTAEnabled = true + }, + accessibilityInputLabel: .defaultSetting + ) + Spacer() + } + .onDisappear { + Task { + await viewModel.saveSettings() + } + } + } +} + +// MARK: - Preview + +#Preview { + LTASettingsView() + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) +} diff --git a/RIADigiDoc/Util/Certificate/CertificateUtil.swift b/RIADigiDoc/Util/Certificate/CertificateUtil.swift index e3d85bc2..2a8349ba 100644 --- a/RIADigiDoc/Util/Certificate/CertificateUtil.swift +++ b/RIADigiDoc/Util/Certificate/CertificateUtil.swift @@ -56,29 +56,30 @@ public struct CertificateUtil: CertificateUtilProtocol, Loggable { } public func getNotValidAfterWithExpiredLabel(cert: Data, expiredLabel: String) -> String { - do { - let certificate = try Certificate(derEncoded: cert.map { $0 }) - let notValidAfterDate = certificate.notValidAfter + guard let notValidAfterDate = getNotValidAfterDate(cert: cert) else { return "" } - let dateTime = DateUtil.getFormattedDateTime( - date: notValidAfterDate, - isUTC: false - ) + let dateTime = DateUtil.getFormattedDateTime(date: notValidAfterDate, isUTC: false) - let calendar = Calendar.current - let todayStart = calendar.startOfDay(for: Date()) - let certStart = calendar.startOfDay(for: notValidAfterDate) + let calendar = Calendar.current + let todayStart = calendar.startOfDay(for: Date()) + let certStart = calendar.startOfDay(for: notValidAfterDate) - if certStart < todayStart { - return "\(dateTime.date) (\(expiredLabel))" - } else { - return dateTime.date - } + if certStart < todayStart { + return "\(dateTime.date) (\(expiredLabel))" + } else { + return dateTime.date + } + } + + public func getNotValidAfterDate(cert: Data) -> Date? { + do { + let certificate = try Certificate(derEncoded: cert.map { $0 }) + return certificate.notValidAfter } catch { CertificateUtil.logger().error( - "Unable to get not valid after from certificate: \(String(reflecting: error))" + "Unable to get not valid after date from certificate: \(String(reflecting: error))" ) - return "" + return nil } } diff --git a/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift b/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift index bc2ad737..ec430383 100644 --- a/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift +++ b/RIADigiDoc/Util/Certificate/CertificateUtilProtocol.swift @@ -27,4 +27,5 @@ public protocol CertificateUtilProtocol: Sendable { func getSubjectAttribute(cert: Data, attribute: ASN1ObjectIdentifier) -> String func getNotValidAfterWithExpiredLabel(cert: Data, expiredLabel: String) -> String func getNotValidDate(_ certData: Data?) throws -> String? + func getNotValidAfterDate(cert: Data) -> Date? } diff --git a/RIADigiDoc/Util/Signature/SignatureUtil.swift b/RIADigiDoc/Util/Signature/SignatureUtil.swift index a69d2814..a4bcc406 100644 --- a/RIADigiDoc/Util/Signature/SignatureUtil.swift +++ b/RIADigiDoc/Util/Signature/SignatureUtil.swift @@ -21,18 +21,18 @@ import SwiftUI import LibdigidocLibSwift struct SignatureUtil: SignatureUtilProtocol { - func getSignatureStatusText(status: SignatureStatus) -> String { + func getSignatureStatusText(status: SignatureStatus, isTimestamp: Bool) -> String { switch status { case .valid: - return "Signature is valid" + return isTimestamp ? "Timestamp is valid" : "Signature is valid" case .warning: - return "Signature is valid with warnings" + return isTimestamp ? "Timestamp is valid" : "Signature is valid with warnings" case .nonQSCD: - return "Signature is valid non qscd" + return isTimestamp ? "Timestamp is valid" : "Signature is valid non qscd" case .invalid: - return "Signature is invalid" + return isTimestamp ? "Timestamp is invalid" : "Signature is invalid" case .unknown: - return "Signature is unknown" + return isTimestamp ? "Timestamp is unknown" : "Signature is unknown" } } } diff --git a/RIADigiDoc/Util/Signature/SignatureUtilProtocol.swift b/RIADigiDoc/Util/Signature/SignatureUtilProtocol.swift index a9335016..f0a120b9 100644 --- a/RIADigiDoc/Util/Signature/SignatureUtilProtocol.swift +++ b/RIADigiDoc/Util/Signature/SignatureUtilProtocol.swift @@ -22,5 +22,5 @@ import LibdigidocLibSwift /// @mockable public protocol SignatureUtilProtocol: Sendable { - func getSignatureStatusText(status: SignatureStatus) -> String + func getSignatureStatusText(status: SignatureStatus, isTimestamp: Bool) -> String } diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index 9b5b54b0..d7c5b193 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -778,11 +778,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { private func getEffectiveContainer( parentContainer: SignedContainerProtocol ) async throws -> SignedContainerProtocol { - let isTimestamped = await sivaRepository.isTimestampedContainer(signedContainer: parentContainer) - let isCades = await parentContainer.isCades() - let isXades = await parentContainer.isXades() - - guard isTimestamped && !isCades && !isXades else { + guard await sivaRepository.shouldOpenNestedTimestampedContainer(parentContainer) else { return parentContainer } diff --git a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift index 4f5c5d92..2f0b9f73 100644 --- a/RIADigiDoc/ViewModel/FileOpeningViewModel.swift +++ b/RIADigiDoc/ViewModel/FileOpeningViewModel.swift @@ -234,10 +234,13 @@ class FileOpeningViewModel: FileOpeningViewModelProtocol, Loggable { } private func handleAsicsSivaConfirmation(parentContainer: SignedContainerProtocol) async throws { - let isTimestampedContainer = await sivaRepository.isTimestampedContainer(signedContainer: parentContainer) - let isCades = await parentContainer.isCades() - let isXades = await parentContainer.isXades() - if isTimestampedContainer && !isCades && !isXades { + // Only open the nested container when SiVa is actually needed (ASiC-S wrapping a DDOC). + // An ASiC-S containing a BDOC must stay a regular ASiC-S container. + let shouldOpenNested = await sivaRepository.shouldOpenNestedTimestampedContainer(parentContainer) + FileOpeningViewModel.logger().info( + "ASiC-S opened. Open nested container: \(shouldOpenNested, privacy: .public)" + ) + if shouldOpenNested { let nestedTimestampedContainer = try await sivaRepository .getTimestampedContainer(parentContainer: parentContainer) sharedContainerViewModel.setSignedContainer(nestedTimestampedContainer) diff --git a/RIADigiDoc/ViewModel/LTASettingsViewModel.swift b/RIADigiDoc/ViewModel/LTASettingsViewModel.swift new file mode 100644 index 00000000..2cbcf353 --- /dev/null +++ b/RIADigiDoc/ViewModel/LTASettingsViewModel.swift @@ -0,0 +1,44 @@ +/* + * 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 + * + */ + +import Foundation + +@Observable +@MainActor +class LTASettingsViewModel: LTASettingsViewModelProtocol { + + var isDefaultLTAEnabled: Bool = false + + private let dataStore: DataStoreProtocol + + init(dataStore: DataStoreProtocol) { + self.dataStore = dataStore + Task { + await loadSettings() + } + } + + func loadSettings() async { + self.isDefaultLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + } + + func saveSettings() async { + await dataStore.setIsDefaultLTAEnabled(isDefaultLTAEnabled) + } +} diff --git a/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift new file mode 100644 index 00000000..c2e9de35 --- /dev/null +++ b/RIADigiDoc/ViewModel/Protocols/LTASettingsViewModelProtocol.swift @@ -0,0 +1,27 @@ +/* + * 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 + * + */ + +/// @mockable +@MainActor +public protocol LTASettingsViewModelProtocol: Sendable { + var isDefaultLTAEnabled: Bool { get } + + func loadSettings() async + func saveSettings() async +} diff --git a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift index 9bbd858e..06182994 100644 --- a/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/SigningViewModelProtocol.swift @@ -45,4 +45,5 @@ public protocol SigningViewModelProtocol: Sendable { func resetErrorMessage() func resetSuccessMessage() func convertToCryptoContainer() async -> Bool + func extendSignatures() async } diff --git a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift index 0cd05bed..5c50ca7f 100644 --- a/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/MobileId/MobileIdViewModel.swift @@ -239,8 +239,16 @@ class MobileIdViewModel: MobileIdViewModelProtocol, Loggable { ) MobileIdViewModel.logger().info("Signature added successfully (Mobile-ID)") - mobileIdSuccessMessageKey = "Signature added" - return updatedContainer + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + let extendedContainer = try await updatedContainer.extendSignature(isLTAEnabled) + mobileIdSuccessMessageKey = "Signature added" + return extendedContainer + } catch { + MobileIdViewModel.logger().error("Unable to extend signature (Mobile-ID): \(error)") + mobileIdErrorMessageKey = "Extending signatures failed" + return updatedContainer + } } catch { MobileIdViewModel.logger().error("Unable to sign container with Mobile-ID: \(error)") handleSignatureAddingError(error) diff --git a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift index bd537e1c..39560219 100644 --- a/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/NFC/NFCViewModel.swift @@ -44,6 +44,8 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { var nfcAlertMessageExtraArguments: [String] = [] var nfcAlertMessageUrl: String? + var signatureExtensionFailed = false + private let nfcCANKeyFilename = Constants.File.nfcCANKey private let dataStore: DataStoreProtocol @@ -384,6 +386,7 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { strings: NFCSessionStrings ) async -> SignedContainerProtocol? { NFCViewModel.logger().info("NFC: Starting NFC signing") + signatureExtensionFailed = false let pin2Data = pin2.data(using: .utf8) guard let pin2Data else { NFCViewModel.logger().error("NFC: Failed to convert PIN2 to Data") @@ -413,7 +416,14 @@ class NFCViewModel: NFCViewModelProtocol, Loggable { strings: strings ) NFCViewModel.logger().info("NFC: Signature added successfully") - return result + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + return try await result.extendSignature(isLTAEnabled) + } catch { + NFCViewModel.logger().error("NFC: Unable to extend signature: \(error)") + signatureExtensionFailed = true + return result + } } catch { NFCViewModel.logger().error("NFC: Signing operation failed") diff --git a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift index b152c285..ca4d753b 100644 --- a/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift +++ b/RIADigiDoc/ViewModel/Signing/SmartId/SmartIdViewModel.swift @@ -120,6 +120,7 @@ class SmartIdViewModel: SmartIdViewModelProtocol, Loggable { } // swiftlint:disable:next cyclomatic_complexity + // swiftlint:disable:next function_body_length func sign( country: SmartIdCountry, personalCode: String, @@ -282,10 +283,18 @@ class SmartIdViewModel: SmartIdViewModelProtocol, Loggable { ) SmartIdViewModel.logger().info("Signature added successfully (Smart-ID)") - smartIdSuccessMessageKey = "Signature added" notificationUtil.removeNotification(id: notificationIdentifier) await endLiveActivity() - return updatedContainer + let isLTAEnabled = await dataStore.getIsDefaultLTAEnabled() + do { + let extendedContainer = try await updatedContainer.extendSignature(isLTAEnabled) + smartIdSuccessMessageKey = "Signature added" + return extendedContainer + } catch { + SmartIdViewModel.logger().error("Unable to extend signature (Smart-ID): \(error)") + smartIdErrorMessageKey = "Extending signatures failed" + return updatedContainer + } } catch { SmartIdViewModel.logger().error("Unable to sign container with Smart-ID: \(error)") handleSignatureAddingError(error) diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index 3096c174..c8d49594 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -28,6 +28,12 @@ import CryptoSwift @MainActor class SigningViewModel: SigningViewModelProtocol, Loggable { + // A BDOC/DDOC extend that was wrapped into an ASiC-S needing SiVa, awaiting the user's view choice. + private struct PendingExtendedContainer { + let url: URL + let isWrapped: Bool + } + var dataFiles: [DataFileWrapper] = [] var signatures: [SignatureWrapper] = [] var timestamps: [SignatureWrapper] = [] @@ -44,6 +50,9 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { var isXadesContainer = false var isLastDataFileRemoved = false var navigateToNestedCryptoContainerView = false + var showExtendSivaConfirmation = false + var showCannotExtendContainerDialog = false + private var pendingExtendedContainer: PendingExtendedContainer? private(set) var containerNotifications: [ContainerNotificationType] = [] private(set) var errorMessage: ToastMessage? private(set) var successMessage: ToastMessage? @@ -582,6 +591,64 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { successMessage = nil } + func extendSignatures() async { + guard let container = signedContainer else { + SigningViewModel.logger().error("Cannot extend signatures: container is nil") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + return + } + let previousMimetype = containerMimetype + SigningViewModel.logger().info("Extending signatures (mimetype: \(previousMimetype, privacy: .public))") + do { + let extended = try await container.extendSignatures() + let isWrapped = await extended.getContainerMimetype() == Constants.MimeType.Asics + && previousMimetype != Constants.MimeType.Asics + SigningViewModel.logger().info("Signatures extended (wrapped into ASiC-S: \(isWrapped, privacy: .public))") + + if let extendedURL = await extended.getRawContainerFile(), + await sivaRepository.isSivaConfirmationNeeded(files: [extendedURL]) { + SigningViewModel.logger().info("Extended container needs SiVa; prompting for view choice") + pendingExtendedContainer = PendingExtendedContainer(url: extendedURL, isWrapped: isWrapped) + showExtendSivaConfirmation = true + } else { + sharedContainerViewModel.removeLastContainer() + sharedContainerViewModel.setSignedContainer(extended) + await loadContainerData(signedContainer: extended) + finishExtend(isWrapped: isWrapped) + } + } catch { + SigningViewModel.logger().error("Unable to extend signatures: \(error)") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + } + } + + func openExtendedContainer(isSivaConfirmed: Bool) async { + showExtendSivaConfirmation = false + guard let pending = pendingExtendedContainer else { return } + pendingExtendedContainer = nil + SigningViewModel.logger().info("Opening extended container (SiVa: \(isSivaConfirmed, privacy: .public))") + do { + sharedContainerViewModel.removeLastContainer() + try await openNestedContainer(fileURL: pending.url, isSivaConfirmed: isSivaConfirmed) + finishExtend(isWrapped: pending.isWrapped) + } catch { + SigningViewModel.logger().error("Unable to open extended container: \(error)") + errorMessage = ToastMessage(key: "Extending signatures failed", args: []) + } + } + + // A wrapped container (BDOC/DDOC that could not be extended in place) informs the user via a dialog; + // an in-place extension just shows the success toast. + private func finishExtend(isWrapped: Bool) { + if isWrapped { + SigningViewModel.logger().info("Container wrapped into ASiC-S. Showing 'cannot be extended' dialog") + showCannotExtendContainerDialog = true + } else { + SigningViewModel.logger().info("Signatures extended") + successMessage = ToastMessage(key: "Signatures extended", args: []) + } + } + func convertToCryptoContainer() async -> Bool { do { guard let container = signedContainer else { @@ -638,9 +705,9 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { isSivaConfirmed: isSivaConfirmed ) - let isXades = await container.isXades() - let isTimestampedContainer = await sivaRepository.isTimestampedContainer(signedContainer: container) - if isSivaConfirmed && isTimestampedContainer && !isXades { + // Only open the nested container when SiVa is actually needed (ASiC-S wrapping a DDOC); an ASiC-S + // wrapping a BDOC stays a regular ASiC-S container (matches DigiDoc4). + if isSivaConfirmed, await sivaRepository.shouldOpenNestedTimestampedContainer(container) { let nestedTimestampedContainer = try await sivaRepository .getTimestampedContainer(parentContainer: container) sharedContainerViewModel.setSignedContainer(nestedTimestampedContainer) diff --git a/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift b/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift index a51a427b..da8bb91c 100644 --- a/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift +++ b/RIADigiDocTests/Domain/Service/Siva/SivaServiceTests.swift @@ -22,6 +22,7 @@ import Testing import LibdigidocLibSwift import CommonsLib import CommonsLibMocks +import CommonsTestShared import LibdigidocLibSwiftMocks import UtilsLibMocks @@ -66,18 +67,36 @@ struct SivaServiceTests { } @Test - func isSivaConfirmationNeeded_returnTrueWithValidSivaContainerMimetype() async { - let file = URL(fileURLWithPath: "/tmp/mockSignedContainer.asics") - - mockMimetypeResolver.mimeTypeHandler = { url in - #expect(url == file) - return Constants.MimeType.Asics + func isSivaConfirmationNeeded_returnTrueForAsicsWrappingDdoc() async throws { + let wrappedDdoc = URL(fileURLWithPath: "/mock/path/wrapped.ddoc") + let mockContainer = try TestContainerUtil.createMockContainer( + with: [wrappedDdoc.lastPathComponent: "Test content"], + containerExtension: "asics") + defer { try? FileManager.default.removeItem(at: mockContainer) } + + mockMimetypeResolver.mimeTypeHandler = { _ in Constants.MimeType.Asics } + mockFileUtil.getFileFromZipFileHandler = { _, name in + name.contains(".ddoc") ? wrappedDdoc : nil } - let isSivaConfirmationNeeded = await service.isSivaConfirmationNeeded(files: [file]) + let isSivaConfirmationNeeded = await service.isSivaConfirmationNeeded(files: [mockContainer]) #expect(isSivaConfirmationNeeded) - #expect(mockMimetypeResolver.mimeTypeCallCount == 1) + } + + @Test + func isSivaConfirmationNeeded_returnFalseForAsicsWrappingBdoc() async throws { + let mockContainer = try TestContainerUtil.createMockContainer( + with: ["wrapped.bdoc": "Test content"], + containerExtension: "asics") + defer { try? FileManager.default.removeItem(at: mockContainer) } + + mockMimetypeResolver.mimeTypeHandler = { _ in Constants.MimeType.Asics } + mockFileUtil.getFileFromZipFileHandler = { _, _ in nil } + + let isSivaConfirmationNeeded = await service.isSivaConfirmationNeeded(files: [mockContainer]) + + #expect(!isSivaConfirmationNeeded) } @Test diff --git a/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift b/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift index b993a655..3bcaa2aa 100644 --- a/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift +++ b/RIADigiDocTests/Util/Certificate/CertificateUtilTests.swift @@ -141,48 +141,6 @@ final class CertificateUtilTests { #expect(!notValidAfter.isEmpty) } - @Test - func getNotValidAfterWithExpiredLabel_successWithExpiredCert() { - let pemString = """ - -----BEGIN CERTIFICATE----- - MIID0jCCArqgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCRUUx - EzARBgNVBAgMClRlc3QgU3RhdGUxEjAQBgNVBAcMCVRlc3QgQ2l0eTEaMBgGA1UE - CgwRVGVzdCBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVRlc3QgVW5pdDEZMBcGA1UE - AwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEyMzU5 - NTlaMIGBMQswCQYDVQQGEwJFRTETMBEGA1UECAwKVGVzdCBTdGF0ZTESMBAGA1UE - BwwJVGVzdCBDaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAGA1UE - CwwJVGVzdCBVbml0MRkwFwYDVQQDDBBUZXN0IENlcnRpZmljYXRlMIIBIjANBgkq - hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0tlBYafLZ4prdlTC+acvl+Z+p8oGxry - oRu3i/FIa8qAS/XHkL7DfmLdHkT3/N8Lclm1mQtVRtcmsMLbiPb6KiywlgZRWh4Z - JHS9t4WtcspxTjLjJ5DilmSPD1lepxCTq2VWECFPVSvh0Uo2Jr2WEWR0Az8MB0g6 - 2qfPz+ywNLKGjMMPOJEgEKpEylos/yU42qOja8r3Ocb2Bid7CbA8y3GzSZmIjS+X - GzmWqTe+4WaBTqAF3Wa5hhcVjbv9uYebgiF3puxYRGnqXR3wjxdH1Dt8VuP/cvic - ynDIEPltZbWIhLMkvIiJirtFQ2MWIJzyTgOg0EO1nFVzHBkn3OsUsQIDAQABo1Mw - UTAdBgNVHQ4EFgQUPcWKjZU/rXkRLqpssv/fUwpCkLAwHwYDVR0jBBgwFoAUPcWK - jZU/rXkRLqpssv/fUwpCkLAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsF - AAOCAQEAQbAPahc6zJ37VxBN4xDn3xXShCxVF+3qMBw7YYJDU39NSnBfCjpCZUbG - QTZAXc3iMB6luO5yoBbUSjX0YT6hqwAyb5s/2Aucv5Y8nLIl/GOAYYCWpZrFVkD0 - fmMt+rR6H5jFTtILsdsfMzmGkJ8xKWyjLUrGGALAzM1VSv1GPV+EpVjLe+bnTJ+E - mvPkvo7970DVF13AqjtE5929PJaMu4t7QHzUItdc74VOVkQ6OcC71nOWhPXlcVi2 - AzZ3Zprwro1aLDmCQRSjBi1git9957oxuQfCtGvoeaP497hWZ4wJK/HRHLlGx1cu - HorK9eEA1jaJ/RRRefXzhjOVHLOuYw== - -----END CERTIFICATE----- - """ - guard let pemData = pemString.data(using: .utf8) else { - Issue.record("Expected pemData to not be nil") - return - } - guard let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { - Issue.record("Expected derData to not be nil") - return - } - let expiredLabel = "Expired" - - let notValidAfter = certificateUtil.getNotValidAfterWithExpiredLabel(cert: derData, expiredLabel: expiredLabel) - - #expect(notValidAfter.contains(expiredLabel)) - } - @Test func getSubjectAttribute_success() { let pemString = """ @@ -301,6 +259,103 @@ final class CertificateUtilTests { } } + @Test + func getNotValidAfterDate_returnsNonNilDateForValidCert() { + let pemString = """ + -----BEGIN CERTIFICATE----- + MIIEDTCCAvWgAwIBAgIUSqorLsfSI1K5t/9YhPnHqf3MBc4wDQYJKoZIhvcNAQEL + BQAwgZUxCzAJBgNVBAYTAkVFMQ4wDAYDVQQIDAVIYXJqdTEQMA4GA1UEBwwHVGFs + bGlubjEOMAwGA1UECgwFTXlPcmcxDzANBgNVBAsMBk15VW5pdDESMBAGA1UEAwwJ + VGVzdCBDZXJ0MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMQ4wDAYD + VQQFEwUxMjM0NTAeFw0yNTEwMTYxMDMyMjJaFw0yNjEwMTYxMDMyMjJaMIGVMQsw + CQYDVQQGEwJFRTEOMAwGA1UECAwFSGFyanUxEDAOBgNVBAcMB1RhbGxpbm4xDjAM + BgNVBAoMBU15T3JnMQ8wDQYDVQQLDAZNeVVuaXQxEjAQBgNVBAMMCVRlc3QgQ2Vy + dDEfMB0GCSqGSIb3DQEJARYQdGVzdEBleGFtcGxlLmNvbTEOMAwGA1UEBRMFMTIz + NDUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWT6mHYaf1xuNus76z + MpVfk3HjI/ZxmswhbPG2LvAxldY7hXaCH8I0qMKorrnUqq3PmWZqG7Wzt78Lu5x8 + SGCJ+fGKH4Fo3cXnGqXQpU1xnwARE08N/g3GlogDH3y0MsbUD/B7Vq218BrWqlEU + BiYI/aO7yfal4tZjVWugBalMWYehHhEOeh0ss4bDjGvEmmPAvRa36UoVbLGrjG95 + vcZv2xCC8YlWyj11X4ci7RZHrbpNrZ21xWr59VU7dTxKIDJ64wfgddryXkiyPHJ3 + R5Syf89qNn0I9SeEuSS13QsF0UEmcT/+rvXf2o8JXNWpPe2AGYVzlWAPHboOKHLI + 2FILAgMBAAGjUzBRMB0GA1UdDgQWBBRLFCXwhwHQ2dmE3xocNJOtPB0DtzAfBgNV + HSMEGDAWgBRLFCXwhwHQ2dmE3xocNJOtPB0DtzAPBgNVHRMBAf8EBTADAQH/MA0G + CSqGSIb3DQEBCwUAA4IBAQB53+FGg8nzYBIq8K/C00GUB2R0XYxUvKsfvecMOcHy + Sl7TKOVZRDaL7Ji3G5CqouAwLFgnXqlf7aKYn4YfWNNXoS9Zm+eFJmvvWYJ/j/C0 + Ntz2mfcMcElrXvCGVnCNiHmkAw193jnya+3JxgbgE8rHoxYMHGwNTZUzCe7QGqw/ + tLdAYRezgyOx5NqaCq1GsOIP3n3eU9k92bMaWM0qtYHroL3H+oIvO0Whdsi2H7Fz + W+L77xnqmKNZDyWwyQ8MsShy8VAJt75TOLPrR6clKou1q3H77ELDtwUAHw7hJF7W + HERHoea8LiuAkZCFBh6fTEd2Wetgble1vYsK/+t+0Y4J + -----END CERTIFICATE----- + """ + guard let pemData = pemString.data(using: .utf8), + let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { + Issue.record("Expected derData to not be nil") + return + } + + let result = certificateUtil.getNotValidAfterDate(cert: derData) + + #expect(result != nil) + } + + @Test + func getNotValidAfterDate_returnsExpiredDateForExpiredCert() { + let pemString = """ + -----BEGIN CERTIFICATE----- + MIID0jCCArqgAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMCRUUx + EzARBgNVBAgMClRlc3QgU3RhdGUxEjAQBgNVBAcMCVRlc3QgQ2l0eTEaMBgGA1UE + CgwRVGVzdCBPcmdhbml6YXRpb24xEjAQBgNVBAsMCVRlc3QgVW5pdDEZMBcGA1UE + AwwQVGVzdCBDZXJ0aWZpY2F0ZTAeFw03MDAxMDEwMDAwMDBaFw03MDAxMDEyMzU5 + NTlaMIGBMQswCQYDVQQGEwJFRTETMBEGA1UECAwKVGVzdCBTdGF0ZTESMBAGA1UE + BwwJVGVzdCBDaXR5MRowGAYDVQQKDBFUZXN0IE9yZ2FuaXphdGlvbjESMBAGA1UE + CwwJVGVzdCBVbml0MRkwFwYDVQQDDBBUZXN0IENlcnRpZmljYXRlMIIBIjANBgkq + hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp0tlBYafLZ4prdlTC+acvl+Z+p8oGxry + oRu3i/FIa8qAS/XHkL7DfmLdHkT3/N8Lclm1mQtVRtcmsMLbiPb6KiywlgZRWh4Z + JHS9t4WtcspxTjLjJ5DilmSPD1lepxCTq2VWECFPVSvh0Uo2Jr2WEWR0Az8MB0g6 + 2qfPz+ywNLKGjMMPOJEgEKpEylos/yU42qOja8r3Ocb2Bid7CbA8y3GzSZmIjS+X + GzmWqTe+4WaBTqAF3Wa5hhcVjbv9uYebgiF3puxYRGnqXR3wjxdH1Dt8VuP/cvic + ynDIEPltZbWIhLMkvIiJirtFQ2MWIJzyTgOg0EO1nFVzHBkn3OsUsQIDAQABo1Mw + UTAdBgNVHQ4EFgQUPcWKjZU/rXkRLqpssv/fUwpCkLAwHwYDVR0jBBgwFoAUPcWK + jZU/rXkRLqpssv/fUwpCkLAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsF + AAOCAQEAQbAPahc6zJ37VxBN4xDn3xXShCxVF+3qMBw7YYJDU39NSnBfCjpCZUbG + QTZAXc3iMB6luO5yoBbUSjX0YT6hqwAyb5s/2Aucv5Y8nLIl/GOAYYCWpZrFVkD0 + fmMt+rR6H5jFTtILsdsfMzmGkJ8xKWyjLUrGGALAzM1VSv1GPV+EpVjLe+bnTJ+E + mvPkvo7970DVF13AqjtE5929PJaMu4t7QHzUItdc74VOVkQ6OcC71nOWhPXlcVi2 + AzZ3Zprwro1aLDmCQRSjBi1git9957oxuQfCtGvoeaP497hWZ4wJK/HRHLlGx1cu + HorK9eEA1jaJ/RRRefXzhjOVHLOuYw== + -----END CERTIFICATE----- + """ + guard let pemData = pemString.data(using: .utf8), + let derData = certificateUtil.pemToDerData(fromPEM: pemData) else { + Issue.record("Expected derData to not be nil") + return + } + + let result = certificateUtil.getNotValidAfterDate(cert: derData) + + guard let date = result else { + Issue.record("Expected date to not be nil") + return + } + #expect(date < Date()) + } + + @Test + func getNotValidAfterDate_returnsNilOnInvalidDer() { + let invalidDer = Data([0x00, 0x01, 0x02]) + + let result = certificateUtil.getNotValidAfterDate(cert: invalidDer) + + #expect(result == nil) + } + + @Test + func getNotValidAfterDate_returnsNilOnEmptyData() { + let result = certificateUtil.getNotValidAfterDate(cert: Data()) + + #expect(result == nil) + } + @Test func getNotValidDate_success() throws { let pemString = """ diff --git a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift index aa3da4fb..6b4c907e 100644 --- a/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/FileOpeningViewModelTests.swift @@ -241,8 +241,10 @@ struct FileOpeningViewModelTests { mockFileOpeningRepository.getValidFilesHandler = { _ in [URL(filePath: "/mock/file.txt")] } + mockMainSignedContainer.getRawContainerFileHandler = { URL(filePath: "/mock/container.asics") } mockFileOpeningRepository.openOrCreateContainerHandler = { _, _ in mockMainSignedContainer } mockSivaRepository.isTimestampedContainerHandler = { _ in true } + mockSivaRepository.isSivaConfirmationNeededHandler = { _ in true } mockSivaRepository.getTimestampedContainerHandler = { _ in mockNestedSignedContainer } mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } mockSharedContainerViewModel.currentContainerHandler = { mockNestedSignedContainer } @@ -262,11 +264,41 @@ struct FileOpeningViewModelTests { await viewModel.handleSivaConfirmation() #expect(mockFileOpeningRepository.openOrCreateContainerCallCount == 1) + #expect(mockSivaRepository.getTimestampedContainerCallCount == 1) #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 1) #expect(viewModel.isSivaConfirmed) #expect(viewModel.isNavigatingToSigningView) } + @Test + func handleSivaConfirmation_asicsWrappingBdocIsNotUnwrapped() async throws { + let mockMainSignedContainer = SignedContainerProtocolMock() + mockMainSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asics } + mockMainSignedContainer.getRawContainerFileHandler = { URL(filePath: "/mock/container.asics") } + + mockFileOpeningRepository.getValidFilesHandler = { _ in [URL(filePath: "/mock/file.txt")] } + mockFileOpeningRepository.openOrCreateContainerHandler = { _, _ in mockMainSignedContainer } + mockSivaRepository.isTimestampedContainerHandler = { _ in true } + mockSivaRepository.isSivaConfirmationNeededHandler = { _ in false } + mockSharedContainerViewModel.getFileOpeningMethodHandler = { .signing } + mockSharedContainerViewModel.currentContainerHandler = { mockMainSignedContainer } + + guard let sharedContainerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: Constants.Identifier.Group + ) else { + Issue.record("Expected a valid shared container URL") + return + } + mockFileManager.containerURLHandler = { _ in sharedContainerURL } + mockFileUtil.removeSharedFilesHandler = { _ in } + + await viewModel.handleFiles() + await viewModel.handleSivaConfirmation() + + #expect(mockSivaRepository.getTimestampedContainerCallCount == 0) + #expect(mockSharedContainerViewModel.setSignedContainerCallCount == 1) + } + @Test func handleSivaConfirmation_successWithNonTimestampedAsicsContainer() async throws { let mockMainSignedContainer = SignedContainerProtocolMock() diff --git a/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift b/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift new file mode 100644 index 00000000..75074a0f --- /dev/null +++ b/RIADigiDocTests/ViewModel/LTASettingsViewModelTests.swift @@ -0,0 +1,81 @@ +/* + * 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 + * + */ + +import Foundation +import Testing + +@MainActor +final class LTASettingsViewModelTests { + + private let mockDataStore: DataStoreProtocolMock + + init() { + mockDataStore = DataStoreProtocolMock() + mockDataStore.getIsDefaultLTAEnabledHandler = { false } + } + + @Test + func isDefaultLTAEnabled_defaultsToFalse() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + #expect(!viewModel.isDefaultLTAEnabled) + } + + @Test + func loadSettings_setsIsDefaultLTAEnabledToTrue() async { + mockDataStore.getIsDefaultLTAEnabledHandler = { true } + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + await viewModel.loadSettings() + + #expect(viewModel.isDefaultLTAEnabled) + } + + @Test + func loadSettings_setsIsDefaultLTAEnabledToFalse() async { + mockDataStore.getIsDefaultLTAEnabledHandler = { false } + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + + await viewModel.loadSettings() + + #expect(!viewModel.isDefaultLTAEnabled) + } + + @Test + func saveSettings_savesEnabledStateToDataStore() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + viewModel.isDefaultLTAEnabled = true + + await viewModel.saveSettings() + + #expect(mockDataStore.setIsDefaultLTAEnabledCallCount == 1) + #expect(mockDataStore.setIsDefaultLTAEnabledArgValues.first == true) + } + + @Test + func saveSettings_savesDisabledStateToDataStore() async { + let viewModel = LTASettingsViewModel(dataStore: mockDataStore) + viewModel.isDefaultLTAEnabled = false + + await viewModel.saveSettings() + + #expect(mockDataStore.setIsDefaultLTAEnabledCallCount == 1) + #expect(mockDataStore.setIsDefaultLTAEnabledArgValues.first == false) + } +} diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index c6a2de2b..65d038ea 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -1472,6 +1472,77 @@ struct SigningViewModelTests: Loggable { #expect(viewModel.dataFiles.count == 2) } + @Test + func extendSignatures_setsSuccessMessageWhenExtensionSucceeds() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { mockSignedContainer } + mockSignedContainer.getDataFilesHandler = { [] } + mockSignedContainer.getSignaturesHandler = { [] } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.successMessage == ToastMessage(key: "Signatures extended", args: [])) + #expect(viewModel.errorMessage == nil) + } + + @Test + func extendSignatures_setsErrorMessageWhenContainerIsNil() async { + await viewModel.extendSignatures() + + #expect(viewModel.errorMessage == ToastMessage(key: "Extending signatures failed", args: [])) + #expect(viewModel.successMessage == nil) + } + + @Test + func extendSignatures_succeedsForDdocContainer() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Ddoc } + mockSignedContainer.extendSignaturesHandler = { mockSignedContainer } + mockSignedContainer.getDataFilesHandler = { [] } + mockSignedContainer.getSignaturesHandler = { [] } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.successMessage == ToastMessage(key: "Signatures extended", args: [])) + #expect(viewModel.errorMessage == nil) + #expect(mockSignedContainer.extendSignaturesCallCount == 1) + } + + @Test + func extendSignatures_setsErrorMessageWhenExtensionThrows() async { + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { + throw DigiDocError.signatureExtensionFailed(ErrorDetail(message: "Extension failed")) + } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.errorMessage == ToastMessage(key: "Extending signatures failed", args: [])) + #expect(viewModel.successMessage == nil) + } + + @Test + func extendSignatures_loadsContainerDataAfterSuccessfulExtension() async { + let extendedContainer = SignedContainerProtocolMock() + let signature = MockSignatureWrapper.mockSignatureWrapper(signatureId: "S-extended") + extendedContainer.getDataFilesHandler = { [] } + extendedContainer.getSignaturesHandler = { [signature] } + + let mockSignedContainer = SignedContainerProtocolMock() + mockSignedContainer.getContainerMimetypeHandler = { Constants.MimeType.Asice } + mockSignedContainer.extendSignaturesHandler = { extendedContainer } + + await viewModel.loadContainerData(signedContainer: mockSignedContainer) + await viewModel.extendSignatures() + + #expect(viewModel.signatures.first?.signatureId == "S-extended") + } + @Test func convertToCryptoContainer_successWithExistingContainer() async throws { let mockSignedContainer = SignedContainerProtocolMock()